From 92469fa3a230bb19c2fdf71e5ada09ee20277304 Mon Sep 17 00:00:00 2001 From: fenix Date: Sun, 26 Mar 2023 16:11:00 +0300 Subject: [PATCH] Initial commit --- .gitignore | 45 + .idea/.gitignore | 8 + .idea/go.imports.xml | 10 + .idea/golinter.xml | 18 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/webarchive.iml | 9 + adapters/processors/headers.go | 53 + adapters/processors/pdf.go | 52 + adapters/processors/pdf_test.go | 29 + adapters/processors/processors.go | 93 ++ adapters/processors/simplesample.pdf | Bin 0 -> 178439 bytes adapters/repository/badger/db.go | 128 +++ adapters/repository/badger/file.go | 40 + adapters/repository/badger/marshal.go | 13 + adapters/repository/badger/page.go | 142 +++ adapters/repository/badger/page_test.go | 60 ++ api/gen.go | 3 + api/openapi.yaml | 174 ++++ api/openapi/oas_cfg_gen.go | 277 ++++++ api/openapi/oas_client_gen.go | 319 ++++++ api/openapi/oas_handlers_gen.go | 331 +++++++ api/openapi/oas_interfaces_gen.go | 6 + api/openapi/oas_json_gen.go | 1151 ++++++++++++++++++++++ api/openapi/oas_middleware_gen.go | 10 + api/openapi/oas_parameters_gen.go | 82 ++ api/openapi/oas_request_decoders_gen.go | 98 ++ api/openapi/oas_request_encoders_gen.go | 32 + api/openapi/oas_response_decoders_gen.go | 267 +++++ api/openapi/oas_response_encoders_gen.go | 87 ++ api/openapi/oas_router_gen.go | 220 +++++ api/openapi/oas_schemas_gen.go | 516 ++++++++++ api/openapi/oas_server_gen.go | 52 + api/openapi/oas_unimplemented_gen.go | 49 + api/openapi/oas_validators_gen.go | 247 +++++ application/application.go | 182 ++++ application/config.go | 38 + application/config_test.go | 42 + cmd/service/main.go | 42 + entity/file.go | 30 + entity/page.go | 99 ++ entity/result.go | 39 + go.mod | 59 ++ go.sum | 231 +++++ ports/rest/converter.go | 132 +++ ports/rest/service.go | 75 ++ 47 files changed, 5610 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/go.imports.xml create mode 100644 .idea/golinter.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/webarchive.iml create mode 100644 adapters/processors/headers.go create mode 100644 adapters/processors/pdf.go create mode 100644 adapters/processors/pdf_test.go create mode 100644 adapters/processors/processors.go create mode 100644 adapters/processors/simplesample.pdf create mode 100644 adapters/repository/badger/db.go create mode 100644 adapters/repository/badger/file.go create mode 100644 adapters/repository/badger/marshal.go create mode 100644 adapters/repository/badger/page.go create mode 100644 adapters/repository/badger/page_test.go create mode 100644 api/gen.go create mode 100644 api/openapi.yaml create mode 100644 api/openapi/oas_cfg_gen.go create mode 100644 api/openapi/oas_client_gen.go create mode 100644 api/openapi/oas_handlers_gen.go create mode 100644 api/openapi/oas_interfaces_gen.go create mode 100644 api/openapi/oas_json_gen.go create mode 100644 api/openapi/oas_middleware_gen.go create mode 100644 api/openapi/oas_parameters_gen.go create mode 100644 api/openapi/oas_request_decoders_gen.go create mode 100644 api/openapi/oas_request_encoders_gen.go create mode 100644 api/openapi/oas_response_decoders_gen.go create mode 100644 api/openapi/oas_response_encoders_gen.go create mode 100644 api/openapi/oas_router_gen.go create mode 100644 api/openapi/oas_schemas_gen.go create mode 100644 api/openapi/oas_server_gen.go create mode 100644 api/openapi/oas_unimplemented_gen.go create mode 100644 api/openapi/oas_validators_gen.go create mode 100644 application/application.go create mode 100644 application/config.go create mode 100644 application/config_test.go create mode 100644 cmd/service/main.go create mode 100644 entity/file.go create mode 100644 entity/page.go create mode 100644 entity/result.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 ports/rest/converter.go create mode 100644 ports/rest/service.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ed58d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +.idea/sonarlint/ +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work +test.http +db diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/golinter.xml b/.idea/golinter.xml new file mode 100644 index 0000000..fcdf3a5 --- /dev/null +++ b/.idea/golinter.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3ce3588 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..bbaef83 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/webarchive.iml b/.idea/webarchive.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/webarchive.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/adapters/processors/headers.go b/adapters/processors/headers.go new file mode 100644 index 0000000..eecb950 --- /dev/null +++ b/adapters/processors/headers.go @@ -0,0 +1,53 @@ +package processors + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "github.com/derfenix/webarchive/entity" +) + +func NewHeaders(client *http.Client) *Headers { + return &Headers{client: client} +} + +type Headers struct { + client *http.Client +} + +func (h *Headers) Process(ctx context.Context, url string) ([]entity.File, error) { + var ( + headersFile entity.File + err error + ) + + req, reqErr := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if reqErr != nil { + return nil, fmt.Errorf("create request: %w", reqErr) + } + + resp, doErr := h.client.Do(req) + if doErr != nil { + return nil, fmt.Errorf("call url: %w", doErr) + } + + headersFile, err = h.newFile(resp.Header) + + if err != nil { + return nil, fmt.Errorf("new file from headers: %w", err) + } + + return []entity.File{headersFile}, nil +} + +func (h *Headers) newFile(headers http.Header) (entity.File, error) { + buf := bytes.NewBuffer(nil) + + if err := headers.Write(buf); err != nil { + return entity.File{}, fmt.Errorf("write headers: %w", err) + } + + return entity.NewFile("headers", buf.Bytes()), nil +} diff --git a/adapters/processors/pdf.go b/adapters/processors/pdf.go new file mode 100644 index 0000000..2098390 --- /dev/null +++ b/adapters/processors/pdf.go @@ -0,0 +1,52 @@ +package processors + +import ( + "context" + "fmt" + "time" + + "github.com/SebastiaanKlippert/go-wkhtmltopdf" + + "github.com/derfenix/webarchive/entity" +) + +func NewPDF() *PDF { + return &PDF{} +} + +type PDF struct{} + +func (P *PDF) Process(_ context.Context, url string) ([]entity.File, error) { + gen, err := wkhtmltopdf.NewPDFGenerator() + if err != nil { + return nil, fmt.Errorf("new pdf generator: %w", err) + } + + gen.Dpi.Set(300) + gen.PageSize.Set(wkhtmltopdf.PageSizeA4) + gen.Orientation.Set(wkhtmltopdf.OrientationPortrait) + gen.Grayscale.Set(false) + gen.Title.Set(url) + + page := wkhtmltopdf.NewPage(url) + page.JavascriptDelay.Set(200) + page.LoadMediaErrorHandling.Set("abort") + page.FooterRight.Set("[page]") + page.HeaderLeft.Set(url) + page.HeaderRight.Set(time.Now().Format(time.DateOnly)) + page.FooterFontSize.Set(10) + page.Zoom.Set(1) + page.ViewportSize.Set("1920x1080") + + gen.AddPage(page) + + // Create PDF document in internal buffer + err = gen.Create() + if err != nil { + return nil, fmt.Errorf("create pdf: %w", err) + } + + file := entity.NewFile("page.pdf", gen.Bytes()) + + return []entity.File{file}, nil +} diff --git a/adapters/processors/pdf_test.go b/adapters/processors/pdf_test.go new file mode 100644 index 0000000..d8971a4 --- /dev/null +++ b/adapters/processors/pdf_test.go @@ -0,0 +1,29 @@ +package processors + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestPDF_Process(t *testing.T) { + t.Parallel() + + if testing.Short() { + t.Skip("skip test with external resource") + } + + files, err := (&PDF{}).Process(context.Background(), "https://github.com/SebastiaanKlippert/go-wkhtmltopdf") + require.NoError(t, err) + require.Len(t, files, 1) + + f := files[0] + fmt.Println("ID ", f.ID) + fmt.Println("Name ", f.Name) + fmt.Println("MimeType ", f.MimeType) + fmt.Println("Size ", f.Size) + fmt.Println("Created ", f.Created.Format(time.RFC3339)) +} diff --git a/adapters/processors/processors.go b/adapters/processors/processors.go new file mode 100644 index 0000000..1b36155 --- /dev/null +++ b/adapters/processors/processors.go @@ -0,0 +1,93 @@ +package processors + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/cookiejar" + "time" + + "github.com/derfenix/webarchive/entity" +) + +type processor interface { + Process(ctx context.Context, url string) ([]entity.File, error) +} + +func NewProcessors() (*Processors, error) { + jar, err := cookiejar.New(&cookiejar.Options{ + PublicSuffixList: nil, + }) + if err != nil { + return nil, fmt.Errorf("create cookie jar: %w", err) + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: time.Second * 10, + KeepAlive: time.Second * 10, + }).DialContext, + MaxIdleConns: 20, + MaxIdleConnsPerHost: 5, + MaxConnsPerHost: 10, + IdleConnTimeout: time.Second * 60, + ResponseHeaderTimeout: time.Second * 20, + MaxResponseHeaderBytes: 1024 * 1024 * 50, + WriteBufferSize: 256, + ReadBufferSize: 1024 * 64, + ForceAttemptHTTP2: true, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) > 3 { + return fmt.Errorf("too many redirects") + } + + return nil + }, + Jar: jar, + Timeout: time.Second * 30, + } + + procs := Processors{ + processors: map[entity.Format]processor{ + entity.FormatHeaders: NewHeaders(httpClient), + entity.FormatPDF: NewPDF(), + }, + } + + return &procs, nil +} + +type Processors struct { + processors map[entity.Format]processor +} + +func (p *Processors) Process(ctx context.Context, format entity.Format, url string) entity.Result { + result := entity.Result{Format: format} + + proc, ok := p.processors[format] + if !ok { + result.Err = fmt.Errorf("no processor registered for format %v", format) + + return result + } + + files, err := proc.Process(ctx, url) + if err != nil { + result.Err = fmt.Errorf("process: %w", err) + + return result + } + + result.Files = files + + return result +} + +func (p *Processors) Override(format entity.Format, proc processor) error { + p.processors[format] = proc + + return nil +} diff --git a/adapters/processors/simplesample.pdf b/adapters/processors/simplesample.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7a9429921831ee51793f1ad90c1ca5ab1f56e53b GIT binary patch literal 178439 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%0AZO`pkf4RYJ&;I|Ko3g>0 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%0D=;rZHP+X(uJggAG` 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 VW&I0X1K1Wyr-Xq)R8|b{{{Wx?m0|z@ literal 0 HcmV?d00001 diff --git a/adapters/repository/badger/db.go b/adapters/repository/badger/db.go new file mode 100644 index 0000000..c1eb5cf --- /dev/null +++ b/adapters/repository/badger/db.go @@ -0,0 +1,128 @@ +package badger + +import ( + "errors" + "fmt" + "os" + "path" + + "github.com/dgraph-io/badger/v4" + "github.com/dgraph-io/badger/v4/options" + "go.uber.org/zap" +) + +const ( + backupStartPath = "backup_start.db" + backupStopPath = "backup_stop.db" +) + +type BackupType uint8 + +const ( + BackupStart BackupType = iota + BackupStop +) + +var ErrDBClosed = fmt.Errorf("database is closed") + +type logger struct { + *zap.SugaredLogger +} + +func (l *logger) Warningf(s string, i ...interface{}) { + l.SugaredLogger.Warnf(s, i...) +} + +func NewBadger(dir string, log *zap.Logger) (*badger.DB, error) { + opts := badger.DefaultOptions(dir) + opts.Logger = &logger{SugaredLogger: log.Sugar()} + opts.Compression = options.ZSTD + opts.ZSTDCompressionLevel = 6 + + db, err := badger.Open(opts) + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + if err := Backup(db, BackupStart); err != nil { + log.Error("backup on start failed", zap.Error(err)) + } + + return db, nil +} + +func Backup(db *badger.DB, bt BackupType) error { + dir := db.Opts().Dir + var backupPath string + + switch bt { + case BackupStart: + backupPath = path.Join(dir, backupStartPath) + case BackupStop: + backupPath = path.Join(dir, backupStopPath) + } + + file, err := os.OpenFile(backupPath, os.O_CREATE|os.O_WRONLY, os.FileMode(0600)) + if err != nil { + return fmt.Errorf("open backup file %s: %w", backupPath, err) + } + defer func() { + _ = file.Close() + }() + + _, err = db.Backup(file, 0) + if err != nil { + return fmt.Errorf("backup: %w", err) + } + + return nil +} + +func Restore(db *badger.DB) error { + dir := db.Opts().Dir + + backupPathStart := path.Join(dir, backupStartPath) + backupPathStop := path.Join(dir, backupStopPath) + + startStat, err := os.Stat(backupPathStart) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("stat file %s: %w", backupPathStart, err) + } + + stopStat, err := os.Stat(backupPathStop) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("stat file %s: %w", backupPathStop, err) + } + + var backupFile string + + switch { + case stopStat != nil && startStat != nil: + if stopStat.ModTime().After(startStat.ModTime()) { + backupFile = backupPathStop + } else { + backupFile = backupPathStart + } + + case stopStat != nil: + backupFile = backupPathStart + + case startStat != nil: + backupFile = backupPathStop + } + + file, err := os.OpenFile(backupFile, os.O_RDONLY, os.FileMode(0600)) + if err != nil { + return fmt.Errorf("open backup file %s: %w", backupFile, err) + } + + defer func() { + _ = file.Close() + }() + + if err := db.Load(file, 20); err != nil { + return fmt.Errorf("load backup: %w", err) + } + + return nil +} diff --git a/adapters/repository/badger/file.go b/adapters/repository/badger/file.go new file mode 100644 index 0000000..6d9512b --- /dev/null +++ b/adapters/repository/badger/file.go @@ -0,0 +1,40 @@ +package badger + +import ( + "context" + "fmt" + + "github.com/dgraph-io/badger/v4" + + "github.com/derfenix/webarchive/entity" +) + +func NewFile(db *badger.DB) *File { + return &File{db: db, prefix: []byte("file:")} +} + +type File struct { + db *badger.DB + prefix []byte +} + +func (f *File) SaveTx(_ context.Context, txn *badger.Txn, file *entity.File) error { + if f.db.IsClosed() { + return ErrDBClosed + } + + marshaled, err := marshal(file) + if err != nil { + return fmt.Errorf("marshal data: %w", err) + } + + if err := txn.Set(f.key(file), marshaled); err != nil { + return fmt.Errorf("put data: %w", err) + } + + return nil +} + +func (f *File) key(file *entity.File) []byte { + return append(f.prefix, []byte(file.ID.String())...) +} diff --git a/adapters/repository/badger/marshal.go b/adapters/repository/badger/marshal.go new file mode 100644 index 0000000..2bbcfda --- /dev/null +++ b/adapters/repository/badger/marshal.go @@ -0,0 +1,13 @@ +package badger + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(b []byte, v interface{}) error { + return msgpack.Unmarshal(b, v) +} diff --git a/adapters/repository/badger/page.go b/adapters/repository/badger/page.go new file mode 100644 index 0000000..ddb0f19 --- /dev/null +++ b/adapters/repository/badger/page.go @@ -0,0 +1,142 @@ +package badger + +import ( + "context" + "fmt" + "sort" + + "github.com/dgraph-io/badger/v4" + "github.com/google/uuid" + + "github.com/derfenix/webarchive/entity" +) + +func NewPage(db *badger.DB, file *File) (*Page, error) { + return &Page{ + db: db, + prefix: []byte("page:"), + file: file, + }, nil +} + +type Page struct { + db *badger.DB + prefix []byte + file *File +} + +func (p *Page) Save(ctx context.Context, site *entity.Page) error { + if p.db.IsClosed() { + return ErrDBClosed + } + + marshaled, err := marshal(site) + if err != nil { + return fmt.Errorf("marshal data: %w", err) + } + + if err := p.db.Update(func(txn *badger.Txn) error { + if err := txn.Set(p.key(site), marshaled); err != nil { + return fmt.Errorf("put data: %w", err) + } + + for i, result := range site.Results.Results() { + for j, file := range result.Files { + if err := p.file.SaveTx(ctx, txn, &file); err != nil { + return fmt.Errorf("save file %d (%s) for result %d: %w", j, file.ID.String(), i, err) + } + } + } + + return nil + }); err != nil { + return fmt.Errorf("update db: %w", err) + } + + return nil +} + +func (p *Page) Get(_ context.Context, id uuid.UUID) (*entity.Page, error) { + site := entity.Page{ID: id} + + err := p.db.View(func(txn *badger.Txn) error { + data, err := txn.Get(p.key(&site)) + if err != nil { + return fmt.Errorf("get data: %w", err) + } + + err = data.Value(func(val []byte) error { + if err := unmarshal(val, &site); err != nil { + return fmt.Errorf("unmarshal data: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("get value: %w", err) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("view: %w", err) + } + + return &site, nil +} + +func (p *Page) ListAll(ctx context.Context) ([]*entity.Page, error) { + pages := make([]*entity.Page, 0, 100) + + err := p.db.View(func(txn *badger.Txn) error { + iterator := txn.NewIterator(badger.DefaultIteratorOptions) + + defer iterator.Close() + + for iterator.Seek(p.prefix); iterator.ValidForPrefix(p.prefix); iterator.Next() { + if err := ctx.Err(); err != nil { + return fmt.Errorf("context canceled: %w", err) + } + + var page entity.Page + + err := iterator.Item().Value(func(val []byte) error { + if err := unmarshal(val, &page); err != nil { + return fmt.Errorf("unmarshal: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("get item: %w", err) + } + + pages = append(pages, &entity.Page{ + ID: page.ID, + URL: page.URL, + Description: page.Description, + Created: page.Created, + Formats: page.Formats, + Version: page.Version, + Status: page.Status, + }) + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("view: %w", err) + } + + sort.Slice(pages, func(i, j int) bool { + return pages[i].Created.After(pages[j].Created) + }) + + return pages, nil +} + +func (p *Page) key(site *entity.Page) []byte { + return append(p.prefix, []byte(site.ID.String())...) +} diff --git a/adapters/repository/badger/page_test.go b/adapters/repository/badger/page_test.go new file mode 100644 index 0000000..75eb2b3 --- /dev/null +++ b/adapters/repository/badger/page_test.go @@ -0,0 +1,60 @@ +package badger + +import ( + "context" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + + "github.com/derfenix/webarchive/entity" +) + +func TestSite(t *testing.T) { + t.Parallel() + + if testing.Short() { + t.Skip("skip db test") + } + + ctx := context.Background() + + tempDir, err := os.MkdirTemp(os.TempDir(), "badger_test") + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, os.RemoveAll(tempDir)) + }) + + log := zaptest.NewLogger(t) + + db, err := NewBadger(tempDir, log.Named("db")) + require.NoError(t, err) + + siteRepo, err := NewPage(db, nil) + require.NoError(t, err) + + t.Run("base path", func(t *testing.T) { + t.Parallel() + + site := entity.NewPage("https://google.com", "Save all google", entity.FormatPDF, entity.FormatSingleFile) + site.Created = site.Created.Truncate(time.Microsecond) + + err := siteRepo.Save(ctx, site) + require.NoError(t, err) + + storedSite, err := siteRepo.Get(ctx, site.ID) + require.NoError(t, err) + + assert.Equal(t, site, storedSite) + + all, err := siteRepo.ListAll(ctx) + require.NoError(t, err) + require.Len(t, all, 1) + + assert.Equal(t, site, all[0]) + }) +} diff --git a/api/gen.go b/api/gen.go new file mode 100644 index 0000000..11fb9b7 --- /dev/null +++ b/api/gen.go @@ -0,0 +1,3 @@ +package api + +//go:generate go run github.com/ogen-go/ogen/cmd/ogen@v0.60.1 --target ./openapi -package openapi --clean openapi.yaml diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..24b0f2e --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,174 @@ +openapi: 3.0.1 +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +servers: + - url: 'https://api.example.com' +paths: + /pages: + get: + operationId: getPages + summary: Get all pages + responses: + 200: + description: All pages data + content: + application/json: + schema: + $ref: '#/components/schemas/pages' + default: + $ref: '#/components/responses/undefinedError' + post: + operationId: addPage + summary: Add new page + requestBody: + content: + application/json: + schema: + type: object + properties: + url: + type: string + description: + type: string + formats: + type: array + items: + $ref: '#/components/schemas/format' + required: + - url + responses: + 201: + description: Page added + content: + application/json: + schema: + $ref: '#/components/schemas/page' + default: + $ref: '#/components/responses/undefinedError' + + /pages/{id}: + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + get: + operationId: getPage + description: Get page details + responses: + 200: + description: Page data + content: + application/json: + schema: + $ref: '#/components/schemas/pageWithResults' + 404: + description: Page not found + default: + $ref: '#/components/responses/undefinedError' + +components: + responses: + undefinedError: + description: Undefined Error + content: + application/json: + schema: + $ref: '#/components/schemas/error' + schemas: + format: + type: string + enum: + - all + - pdf + - single_page + - headers + error: + type: object + properties: + message: + type: string + localized: + type: string + required: + - message + pages: + type: array + items: + $ref: '#/components/schemas/page' + page: + type: object + properties: + id: + type: string + format: uuid + url: + type: string + created: + type: string + format: date-time + formats: + type: array + items: + $ref: '#/components/schemas/format' + status: + $ref: '#/components/schemas/status' + required: + - id + - url + - formats + - status + - created + result: + type: object + properties: + format: + $ref: '#/components/schemas/format' + error: + type: string + files: + type: array + items: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + mimetype: + type: string + size: + type: integer + format: int64 + required: + - id + - name + - mimetype + - size + required: + - format + - files + pageWithResults: + allOf: + - $ref: '#/components/schemas/page' + - type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/result' + required: + - results + status: + type: string + enum: + - new + - processing + - done + - failed + - with_errors diff --git a/api/openapi/oas_cfg_gen.go b/api/openapi/oas_cfg_gen.go new file mode 100644 index 0000000..3bbddd8 --- /dev/null +++ b/api/openapi/oas_cfg_gen.go @@ -0,0 +1,277 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "net/http" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/instrument" + "go.opentelemetry.io/otel/trace" + + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/otelogen" +) + +var ( + // Allocate option closure once. + clientSpanKind = trace.WithSpanKind(trace.SpanKindClient) + // Allocate option closure once. + serverSpanKind = trace.WithSpanKind(trace.SpanKindServer) +) + +type ( + optionFunc[C any] func(*C) + otelOptionFunc func(*otelConfig) +) + +type otelConfig struct { + TracerProvider trace.TracerProvider + Tracer trace.Tracer + MeterProvider metric.MeterProvider + Meter metric.Meter +} + +func (cfg *otelConfig) initOTEL() { + if cfg.TracerProvider == nil { + cfg.TracerProvider = otel.GetTracerProvider() + } + if cfg.MeterProvider == nil { + cfg.MeterProvider = metric.NewNoopMeterProvider() + } + cfg.Tracer = cfg.TracerProvider.Tracer(otelogen.Name, + trace.WithInstrumentationVersion(otelogen.SemVersion()), + ) + cfg.Meter = cfg.MeterProvider.Meter(otelogen.Name) +} + +// ErrorHandler is error handler. +type ErrorHandler = ogenerrors.ErrorHandler + +type serverConfig struct { + otelConfig + NotFound http.HandlerFunc + MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string) + ErrorHandler ErrorHandler + Prefix string + Middleware Middleware + MaxMultipartMemory int64 +} + +// ServerOption is server config option. +type ServerOption interface { + applyServer(*serverConfig) +} + +var _ = []ServerOption{ + (optionFunc[serverConfig])(nil), + (otelOptionFunc)(nil), +} + +func (o optionFunc[C]) applyServer(c *C) { + o(c) +} + +func (o otelOptionFunc) applyServer(c *serverConfig) { + o(&c.otelConfig) +} + +func newServerConfig(opts ...ServerOption) serverConfig { + cfg := serverConfig{ + NotFound: http.NotFound, + MethodNotAllowed: func(w http.ResponseWriter, r *http.Request, allowed string) { + w.Header().Set("Allow", allowed) + w.WriteHeader(http.StatusMethodNotAllowed) + }, + ErrorHandler: ogenerrors.DefaultErrorHandler, + Middleware: nil, + MaxMultipartMemory: 32 << 20, // 32 MB + } + for _, opt := range opts { + opt.applyServer(&cfg) + } + cfg.initOTEL() + return cfg +} + +type baseServer struct { + cfg serverConfig + requests instrument.Int64Counter + errors instrument.Int64Counter + duration instrument.Int64Histogram +} + +func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) { + s.cfg.NotFound(w, r) +} + +func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) { + s.cfg.MethodNotAllowed(w, r, allowed) +} + +func (cfg serverConfig) baseServer() (s baseServer, err error) { + s = baseServer{cfg: cfg} + if s.requests, err = s.cfg.Meter.Int64Counter(otelogen.ServerRequestCount); err != nil { + return s, err + } + if s.errors, err = s.cfg.Meter.Int64Counter(otelogen.ServerErrorsCount); err != nil { + return s, err + } + if s.duration, err = s.cfg.Meter.Int64Histogram(otelogen.ServerDuration); err != nil { + return s, err + } + return s, nil +} + +type clientConfig struct { + otelConfig + Client ht.Client +} + +// ClientOption is client config option. +type ClientOption interface { + applyClient(*clientConfig) +} + +var _ = []ClientOption{ + (optionFunc[clientConfig])(nil), + (otelOptionFunc)(nil), +} + +func (o optionFunc[C]) applyClient(c *C) { + o(c) +} + +func (o otelOptionFunc) applyClient(c *clientConfig) { + o(&c.otelConfig) +} + +func newClientConfig(opts ...ClientOption) clientConfig { + cfg := clientConfig{ + Client: http.DefaultClient, + } + for _, opt := range opts { + opt.applyClient(&cfg) + } + cfg.initOTEL() + return cfg +} + +type baseClient struct { + cfg clientConfig + requests instrument.Int64Counter + errors instrument.Int64Counter + duration instrument.Int64Histogram +} + +func (cfg clientConfig) baseClient() (c baseClient, err error) { + c = baseClient{cfg: cfg} + if c.requests, err = c.cfg.Meter.Int64Counter(otelogen.ClientRequestCount); err != nil { + return c, err + } + if c.errors, err = c.cfg.Meter.Int64Counter(otelogen.ClientErrorsCount); err != nil { + return c, err + } + if c.duration, err = c.cfg.Meter.Int64Histogram(otelogen.ClientDuration); err != nil { + return c, err + } + return c, nil +} + +// Option is config option. +type Option interface { + ServerOption + ClientOption +} + +// WithTracerProvider specifies a tracer provider to use for creating a tracer. +// +// If none is specified, the global provider is used. +func WithTracerProvider(provider trace.TracerProvider) Option { + return otelOptionFunc(func(cfg *otelConfig) { + if provider != nil { + cfg.TracerProvider = provider + } + }) +} + +// WithMeterProvider specifies a meter provider to use for creating a meter. +// +// If none is specified, the metric.NewNoopMeterProvider is used. +func WithMeterProvider(provider metric.MeterProvider) Option { + return otelOptionFunc(func(cfg *otelConfig) { + if provider != nil { + cfg.MeterProvider = provider + } + }) +} + +// WithClient specifies http client to use. +func WithClient(client ht.Client) ClientOption { + return optionFunc[clientConfig](func(cfg *clientConfig) { + if client != nil { + cfg.Client = client + } + }) +} + +// WithNotFound specifies Not Found handler to use. +func WithNotFound(notFound http.HandlerFunc) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if notFound != nil { + cfg.NotFound = notFound + } + }) +} + +// WithMethodNotAllowed specifies Method Not Allowed handler to use. +func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if methodNotAllowed != nil { + cfg.MethodNotAllowed = methodNotAllowed + } + }) +} + +// WithErrorHandler specifies error handler to use. +func WithErrorHandler(h ErrorHandler) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if h != nil { + cfg.ErrorHandler = h + } + }) +} + +// WithPathPrefix specifies server path prefix. +func WithPathPrefix(prefix string) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + cfg.Prefix = prefix + }) +} + +// WithMiddleware specifies middlewares to use. +func WithMiddleware(m ...Middleware) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + switch len(m) { + case 0: + cfg.Middleware = nil + case 1: + cfg.Middleware = m[0] + default: + cfg.Middleware = middleware.ChainMiddlewares(m...) + } + }) +} + +// WithMaxMultipartMemory specifies limit of memory for storing file parts. +// File parts which can't be stored in memory will be stored on disk in temporary files. +func WithMaxMultipartMemory(max int64) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if max > 0 { + cfg.MaxMultipartMemory = max + } + }) +} diff --git a/api/openapi/oas_client_gen.go b/api/openapi/oas_client_gen.go new file mode 100644 index 0000000..912bc8c --- /dev/null +++ b/api/openapi/oas_client_gen.go @@ -0,0 +1,319 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "context" + "net/url" + "strings" + "time" + + "github.com/go-faster/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + "github.com/ogen-go/ogen/conv" + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/otelogen" + "github.com/ogen-go/ogen/uri" +) + +// Client implements OAS client. +type Client struct { + serverURL *url.URL + baseClient +} +type errorHandler interface { + NewError(ctx context.Context, err error) *ErrorStatusCode +} + +var _ Handler = struct { + errorHandler + *Client +}{} + +func trimTrailingSlashes(u *url.URL) { + u.Path = strings.TrimRight(u.Path, "/") + u.RawPath = strings.TrimRight(u.RawPath, "/") +} + +// NewClient initializes new Client defined by OAS. +func NewClient(serverURL string, opts ...ClientOption) (*Client, error) { + u, err := url.Parse(serverURL) + if err != nil { + return nil, err + } + trimTrailingSlashes(u) + + c, err := newClientConfig(opts...).baseClient() + if err != nil { + return nil, err + } + return &Client{ + serverURL: u, + baseClient: c, + }, nil +} + +type serverURLKey struct{} + +// WithServerURL sets context key to override server URL. +func WithServerURL(ctx context.Context, u *url.URL) context.Context { + return context.WithValue(ctx, serverURLKey{}, u) +} + +func (c *Client) requestURL(ctx context.Context) *url.URL { + u, ok := ctx.Value(serverURLKey{}).(*url.URL) + if !ok { + return c.serverURL + } + return u +} + +// AddPage invokes addPage operation. +// +// Add new page. +// +// POST /pages +func (c *Client) AddPage(ctx context.Context, request OptAddPageReq) (*Page, error) { + res, err := c.sendAddPage(ctx, request) + _ = res + return res, err +} + +func (c *Client) sendAddPage(ctx context.Context, request OptAddPageReq) (res *Page, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("addPage"), + } + // Validate request before sending. + if err := func() error { + if request.Set { + if err := func() error { + if err := request.Value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, otelAttrs...) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, "AddPage", + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, otelAttrs...) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/pages" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u, nil) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeAddPageRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeAddPageResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// GetPage invokes getPage operation. +// +// Get page details. +// +// GET /pages/{id} +func (c *Client) GetPage(ctx context.Context, params GetPageParams) (GetPageRes, error) { + res, err := c.sendGetPage(ctx, params) + _ = res + return res, err +} + +func (c *Client) sendGetPage(ctx context.Context, params GetPageParams) (res GetPageRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getPage"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, otelAttrs...) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, "GetPage", + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, otelAttrs...) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [2]string + pathParts[0] = "/pages/" + { + // Encode "id" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "id", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.UUIDToString(params.ID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u, nil) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeGetPageResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// GetPages invokes getPages operation. +// +// Get all pages. +// +// GET /pages +func (c *Client) GetPages(ctx context.Context) (Pages, error) { + res, err := c.sendGetPages(ctx) + _ = res + return res, err +} + +func (c *Client) sendGetPages(ctx context.Context) (res Pages, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getPages"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, otelAttrs...) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, "GetPages", + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, otelAttrs...) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/pages" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u, nil) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeGetPagesResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} diff --git a/api/openapi/oas_handlers_gen.go b/api/openapi/oas_handlers_gen.go new file mode 100644 index 0000000..5676049 --- /dev/null +++ b/api/openapi/oas_handlers_gen.go @@ -0,0 +1,331 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "context" + "net/http" + "time" + + "github.com/go-faster/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" + + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/otelogen" +) + +// handleAddPageRequest handles addPage operation. +// +// Add new page. +// +// POST /pages +func (s *Server) handleAddPageRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("addPage"), + semconv.HTTPMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/pages"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "AddPage", + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + s.requests.Add(ctx, 1, otelAttrs...) + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, otelAttrs...) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: "AddPage", + ID: "addPage", + } + ) + request, close, err := s.decodeAddPageRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *Page + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "AddPage", + OperationID: "addPage", + Body: request, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = OptAddPageReq + Params = struct{} + Response = *Page + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.AddPage(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.AddPage(ctx, request) + } + if err != nil { + recordError("Internal", err) + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + encodeErrorResponse(errRes, w, span) + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + encodeErrorResponse(s.h.NewError(ctx, err), w, span) + return + } + + if err := encodeAddPageResponse(response, w, span); err != nil { + recordError("EncodeResponse", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } +} + +// handleGetPageRequest handles getPage operation. +// +// Get page details. +// +// GET /pages/{id} +func (s *Server) handleGetPageRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getPage"), + semconv.HTTPMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/pages/{id}"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "GetPage", + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + s.requests.Add(ctx, 1, otelAttrs...) + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, otelAttrs...) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: "GetPage", + ID: "getPage", + } + ) + params, err := decodeGetPageParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response GetPageRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "GetPage", + OperationID: "getPage", + Body: nil, + Params: middleware.Parameters{ + { + Name: "id", + In: "path", + }: params.ID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetPageParams + Response = GetPageRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetPageParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetPage(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetPage(ctx, params) + } + if err != nil { + recordError("Internal", err) + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + encodeErrorResponse(errRes, w, span) + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + encodeErrorResponse(s.h.NewError(ctx, err), w, span) + return + } + + if err := encodeGetPageResponse(response, w, span); err != nil { + recordError("EncodeResponse", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } +} + +// handleGetPagesRequest handles getPages operation. +// +// Get all pages. +// +// GET /pages +func (s *Server) handleGetPagesRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getPages"), + semconv.HTTPMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/pages"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "GetPages", + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + s.requests.Add(ctx, 1, otelAttrs...) + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, otelAttrs...) + } + err error + ) + + var response Pages + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "GetPages", + OperationID: "getPages", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = Pages + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetPages(ctx) + return response, err + }, + ) + } else { + response, err = s.h.GetPages(ctx) + } + if err != nil { + recordError("Internal", err) + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + encodeErrorResponse(errRes, w, span) + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + encodeErrorResponse(s.h.NewError(ctx, err), w, span) + return + } + + if err := encodeGetPagesResponse(response, w, span); err != nil { + recordError("EncodeResponse", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } +} diff --git a/api/openapi/oas_interfaces_gen.go b/api/openapi/oas_interfaces_gen.go new file mode 100644 index 0000000..5bbe0d4 --- /dev/null +++ b/api/openapi/oas_interfaces_gen.go @@ -0,0 +1,6 @@ +// Code generated by ogen, DO NOT EDIT. +package openapi + +type GetPageRes interface { + getPageRes() +} diff --git a/api/openapi/oas_json_gen.go b/api/openapi/oas_json_gen.go new file mode 100644 index 0000000..8406603 --- /dev/null +++ b/api/openapi/oas_json_gen.go @@ -0,0 +1,1151 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "math/bits" + "strconv" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + + "github.com/ogen-go/ogen/json" + "github.com/ogen-go/ogen/validate" +) + +// Encode implements json.Marshaler. +func (s *AddPageReq) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *AddPageReq) encodeFields(e *jx.Encoder) { + { + + e.FieldStart("url") + e.Str(s.URL) + } + { + if s.Description.Set { + e.FieldStart("description") + s.Description.Encode(e) + } + } + { + if s.Formats != nil { + e.FieldStart("formats") + e.ArrStart() + for _, elem := range s.Formats { + elem.Encode(e) + } + e.ArrEnd() + } + } +} + +var jsonFieldsNameOfAddPageReq = [3]string{ + 0: "url", + 1: "description", + 2: "formats", +} + +// Decode decodes AddPageReq from json. +func (s *AddPageReq) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode AddPageReq to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "url": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.URL = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"url\"") + } + case "description": + if err := func() error { + s.Description.Reset() + if err := s.Description.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"description\"") + } + case "formats": + if err := func() error { + s.Formats = make([]Format, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Format + if err := elem.Decode(d); err != nil { + return err + } + s.Formats = append(s.Formats, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"formats\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode AddPageReq") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + 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(jsonFieldsNameOfAddPageReq) { + name = jsonFieldsNameOfAddPageReq[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 *AddPageReq) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *AddPageReq) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *Error) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *Error) encodeFields(e *jx.Encoder) { + { + + e.FieldStart("message") + e.Str(s.Message) + } + { + if s.Localized.Set { + e.FieldStart("localized") + s.Localized.Encode(e) + } + } +} + +var jsonFieldsNameOfError = [2]string{ + 0: "message", + 1: "localized", +} + +// Decode decodes Error from json. +func (s *Error) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Error to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "message": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.Message = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"message\"") + } + case "localized": + if err := func() error { + s.Localized.Reset() + if err := s.Localized.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"localized\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode Error") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + 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(jsonFieldsNameOfError) { + name = jsonFieldsNameOfError[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 *Error) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Error) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Format as json. +func (s Format) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes Format from json. +func (s *Format) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Format to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch Format(v) { + case FormatAll: + *s = FormatAll + case FormatPdf: + *s = FormatPdf + case FormatSinglePage: + *s = FormatSinglePage + case FormatHeaders: + *s = FormatHeaders + default: + *s = Format(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s Format) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Format) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes AddPageReq as json. +func (o OptAddPageReq) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes AddPageReq from json. +func (o *OptAddPageReq) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptAddPageReq to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptAddPageReq) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptAddPageReq) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes string as json. +func (o OptString) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Str(string(o.Value)) +} + +// Decode decodes string from json. +func (o *OptString) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptString to nil") + } + o.Set = true + v, err := d.Str() + if err != nil { + return err + } + o.Value = string(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptString) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptString) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *Page) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *Page) encodeFields(e *jx.Encoder) { + { + + e.FieldStart("id") + json.EncodeUUID(e, s.ID) + } + { + + e.FieldStart("url") + e.Str(s.URL) + } + { + + e.FieldStart("created") + json.EncodeDateTime(e, s.Created) + } + { + + e.FieldStart("formats") + e.ArrStart() + for _, elem := range s.Formats { + elem.Encode(e) + } + e.ArrEnd() + } + { + + e.FieldStart("status") + s.Status.Encode(e) + } +} + +var jsonFieldsNameOfPage = [5]string{ + 0: "id", + 1: "url", + 2: "created", + 3: "formats", + 4: "status", +} + +// Decode decodes Page from json. +func (s *Page) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Page to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "id": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := json.DecodeUUID(d) + s.ID = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"id\"") + } + case "url": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.URL = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"url\"") + } + case "created": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := json.DecodeDateTime(d) + s.Created = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"created\"") + } + case "formats": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + s.Formats = make([]Format, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Format + if err := elem.Decode(d); err != nil { + return err + } + s.Formats = append(s.Formats, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"formats\"") + } + case "status": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + if err := s.Status.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"status\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode Page") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00011111, + } { + 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(jsonFieldsNameOfPage) { + name = jsonFieldsNameOfPage[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 *Page) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Page) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *PageWithResults) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PageWithResults) encodeFields(e *jx.Encoder) { + { + + e.FieldStart("id") + json.EncodeUUID(e, s.ID) + } + { + + e.FieldStart("url") + e.Str(s.URL) + } + { + + e.FieldStart("created") + json.EncodeDateTime(e, s.Created) + } + { + + e.FieldStart("formats") + e.ArrStart() + for _, elem := range s.Formats { + elem.Encode(e) + } + e.ArrEnd() + } + { + + e.FieldStart("status") + s.Status.Encode(e) + } + { + + e.FieldStart("results") + e.ArrStart() + for _, elem := range s.Results { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfPageWithResults = [6]string{ + 0: "id", + 1: "url", + 2: "created", + 3: "formats", + 4: "status", + 5: "results", +} + +// Decode decodes PageWithResults from json. +func (s *PageWithResults) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PageWithResults to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "id": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := json.DecodeUUID(d) + s.ID = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"id\"") + } + case "url": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.URL = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"url\"") + } + case "created": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := json.DecodeDateTime(d) + s.Created = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"created\"") + } + case "formats": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + s.Formats = make([]Format, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Format + if err := elem.Decode(d); err != nil { + return err + } + s.Formats = append(s.Formats, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"formats\"") + } + case "status": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + if err := s.Status.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"status\"") + } + case "results": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + s.Results = make([]Result, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Result + if err := elem.Decode(d); err != nil { + return err + } + s.Results = append(s.Results, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"results\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PageWithResults") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00111111, + } { + 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(jsonFieldsNameOfPageWithResults) { + name = jsonFieldsNameOfPageWithResults[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 *PageWithResults) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PageWithResults) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Pages as json. +func (s Pages) Encode(e *jx.Encoder) { + unwrapped := []Page(s) + + e.ArrStart() + for _, elem := range unwrapped { + elem.Encode(e) + } + e.ArrEnd() +} + +// Decode decodes Pages from json. +func (s *Pages) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Pages to nil") + } + var unwrapped []Page + if err := func() error { + unwrapped = make([]Page, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Page + if err := elem.Decode(d); err != nil { + return err + } + unwrapped = append(unwrapped, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = Pages(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s Pages) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Pages) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *Result) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *Result) encodeFields(e *jx.Encoder) { + { + + e.FieldStart("format") + s.Format.Encode(e) + } + { + if s.Error.Set { + e.FieldStart("error") + s.Error.Encode(e) + } + } + { + + e.FieldStart("files") + e.ArrStart() + for _, elem := range s.Files { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfResult = [3]string{ + 0: "format", + 1: "error", + 2: "files", +} + +// Decode decodes Result from json. +func (s *Result) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Result to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "format": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Format.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"format\"") + } + case "error": + if err := func() error { + s.Error.Reset() + if err := s.Error.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"error\"") + } + case "files": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + s.Files = make([]ResultFilesItem, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem ResultFilesItem + if err := elem.Decode(d); err != nil { + return err + } + s.Files = append(s.Files, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"files\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode Result") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000101, + } { + 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(jsonFieldsNameOfResult) { + name = jsonFieldsNameOfResult[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 *Result) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Result) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ResultFilesItem) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ResultFilesItem) encodeFields(e *jx.Encoder) { + { + + e.FieldStart("id") + json.EncodeUUID(e, s.ID) + } + { + + e.FieldStart("name") + e.Str(s.Name) + } + { + + e.FieldStart("mimetype") + e.Str(s.Mimetype) + } + { + + e.FieldStart("size") + e.Int64(s.Size) + } +} + +var jsonFieldsNameOfResultFilesItem = [4]string{ + 0: "id", + 1: "name", + 2: "mimetype", + 3: "size", +} + +// Decode decodes ResultFilesItem from json. +func (s *ResultFilesItem) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ResultFilesItem to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "id": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := json.DecodeUUID(d) + s.ID = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"id\"") + } + case "name": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Name = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"name\"") + } + case "mimetype": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := d.Str() + s.Mimetype = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"mimetype\"") + } + case "size": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := d.Int64() + s.Size = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"size\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ResultFilesItem") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00001111, + } { + 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(jsonFieldsNameOfResultFilesItem) { + name = jsonFieldsNameOfResultFilesItem[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 *ResultFilesItem) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ResultFilesItem) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Status as json. +func (s Status) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes Status from json. +func (s *Status) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Status to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch Status(v) { + case StatusNew: + *s = StatusNew + case StatusProcessing: + *s = StatusProcessing + case StatusDone: + *s = StatusDone + case StatusFailed: + *s = StatusFailed + case StatusWithErrors: + *s = StatusWithErrors + default: + *s = Status(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s Status) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Status) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} diff --git a/api/openapi/oas_middleware_gen.go b/api/openapi/oas_middleware_gen.go new file mode 100644 index 0000000..083fe0e --- /dev/null +++ b/api/openapi/oas_middleware_gen.go @@ -0,0 +1,10 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "github.com/ogen-go/ogen/middleware" +) + +// Middleware is middleware type. +type Middleware = middleware.Middleware diff --git a/api/openapi/oas_parameters_gen.go b/api/openapi/oas_parameters_gen.go new file mode 100644 index 0000000..a0504da --- /dev/null +++ b/api/openapi/oas_parameters_gen.go @@ -0,0 +1,82 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "net/http" + "net/url" + + "github.com/go-faster/errors" + "github.com/google/uuid" + + "github.com/ogen-go/ogen/conv" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/uri" + "github.com/ogen-go/ogen/validate" +) + +// GetPageParams is parameters of getPage operation. +type GetPageParams struct { + ID uuid.UUID +} + +func unpackGetPageParams(packed middleware.Parameters) (params GetPageParams) { + { + key := middleware.ParameterKey{ + Name: "id", + In: "path", + } + params.ID = packed[key].(uuid.UUID) + } + return params +} + +func decodeGetPageParams(args [1]string, argsEscaped bool, r *http.Request) (params GetPageParams, _ error) { + // Decode path: id. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "id", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUUID(val) + if err != nil { + return err + } + + params.ID = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "id", + In: "path", + Err: err, + } + } + return params, nil +} diff --git a/api/openapi/oas_request_decoders_gen.go b/api/openapi/oas_request_decoders_gen.go new file mode 100644 index 0000000..bdd344b --- /dev/null +++ b/api/openapi/oas_request_decoders_gen.go @@ -0,0 +1,98 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "io" + "mime" + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "go.uber.org/multierr" + + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/validate" +) + +func (s *Server) decodeAddPageRequest(r *http.Request) ( + req OptAddPageReq, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + if _, ok := r.Header["Content-Type"]; !ok && r.ContentLength == 0 { + return req, close, nil + } + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, nil + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, nil + } + + d := jx.DecodeBytes(buf) + + var request OptAddPageReq + if err := func() error { + request.Reset() + if err := request.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 req, close, err + } + if err := func() error { + if request.Set { + if err := func() error { + if err := request.Value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return req, close, errors.Wrap(err, "validate") + } + return request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} diff --git a/api/openapi/oas_request_encoders_gen.go b/api/openapi/oas_request_encoders_gen.go new file mode 100644 index 0000000..f3f6f7c --- /dev/null +++ b/api/openapi/oas_request_encoders_gen.go @@ -0,0 +1,32 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "bytes" + "net/http" + + "github.com/go-faster/jx" + + ht "github.com/ogen-go/ogen/http" +) + +func encodeAddPageRequest( + req OptAddPageReq, + r *http.Request, +) error { + const contentType = "application/json" + if !req.Set { + // Keep request with empty body if value is not set. + return nil + } + e := jx.GetEncoder() + { + if req.Set { + req.Encode(e) + } + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} diff --git a/api/openapi/oas_response_decoders_gen.go b/api/openapi/oas_response_decoders_gen.go new file mode 100644 index 0000000..b50c296 --- /dev/null +++ b/api/openapi/oas_response_decoders_gen.go @@ -0,0 +1,267 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "io" + "mime" + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/validate" +) + +func decodeAddPageResponse(resp *http.Response) (res *Page, err error) { + switch resp.StatusCode { + case 201: + // Code 201. + 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 Page + 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) { + 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 Error + 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 &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrap(err, "default") + } + return res, errors.Wrap(defRes, "error") +} + +func decodeGetPageResponse(resp *http.Response) (res GetPageRes, err error) { + switch resp.StatusCode { + case 200: + // Code 200. + 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 PageWithResults + 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) + } + case 404: + // Code 404. + return &GetPageNotFound{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + 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 Error + 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 &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrap(err, "default") + } + return res, errors.Wrap(defRes, "error") +} + +func decodeGetPagesResponse(resp *http.Response) (res Pages, err error) { + switch resp.StatusCode { + case 200: + // Code 200. + 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 Pages + 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) { + 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 Error + 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 &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrap(err, "default") + } + return res, errors.Wrap(defRes, "error") +} diff --git a/api/openapi/oas_response_encoders_gen.go b/api/openapi/oas_response_encoders_gen.go new file mode 100644 index 0000000..54265cd --- /dev/null +++ b/api/openapi/oas_response_encoders_gen.go @@ -0,0 +1,87 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "go.opentelemetry.io/otel/codes" + "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)) + + e := jx.GetEncoder() + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + return nil +} + +func encodeGetPageResponse(response GetPageRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *PageWithResults: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := jx.GetEncoder() + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + return nil + + case *GetPageNotFound: + w.WriteHeader(404) + span.SetStatus(codes.Error, http.StatusText(404)) + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeGetPagesResponse(response Pages, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := jx.GetEncoder() + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + return nil +} + +func encodeErrorResponse(response *ErrorStatusCode, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json") + code := response.StatusCode + if code == 0 { + // Set default status code. + code = http.StatusOK + } + w.WriteHeader(code) + st := http.StatusText(code) + if code >= http.StatusBadRequest { + span.SetStatus(codes.Error, st) + } else { + span.SetStatus(codes.Ok, st) + } + + e := jx.GetEncoder() + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + return nil + +} diff --git a/api/openapi/oas_router_gen.go b/api/openapi/oas_router_gen.go new file mode 100644 index 0000000..04d5d8c --- /dev/null +++ b/api/openapi/oas_router_gen.go @@ -0,0 +1,220 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "net/http" + "net/url" + "strings" + + "github.com/ogen-go/ogen/uri" +) + +// ServeHTTP serves http request as defined by OpenAPI v3 specification, +// calling handler that matches the path or returning not found error. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + elem := r.URL.Path + elemIsEscaped := false + if rawPath := r.URL.RawPath; rawPath != "" { + if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { + elem = normalized + elemIsEscaped = strings.ContainsRune(elem, '%') + } + } + if prefix := s.cfg.Prefix; len(prefix) > 0 { + if strings.HasPrefix(elem, prefix) { + // Cut prefix from the path. + elem = strings.TrimPrefix(elem, prefix) + } else { + // Prefix doesn't match. + s.notFound(w, r) + return + } + } + if len(elem) == 0 { + s.notFound(w, r) + return + } + args := [1]string{} + + // Static code generated router with unwrapped path search. + switch { + default: + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/pages" + if l := len("/pages"); len(elem) >= l && elem[0:l] == "/pages" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleGetPagesRequest([0]string{}, elemIsEscaped, w, r) + case "POST": + s.handleAddPageRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET,POST") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "id" + // Leaf parameter + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetPageRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } + } + } + } + s.notFound(w, r) +} + +// Route is route object. +type Route struct { + name string + operationID string + pathPattern string + count int + args [1]string +} + +// Name returns ogen operation name. +// +// It is guaranteed to be unique and not empty. +func (r Route) Name() string { + return r.name +} + +// OperationID returns OpenAPI operationId. +func (r Route) OperationID() string { + return r.operationID +} + +// PathPattern returns OpenAPI path. +func (r Route) PathPattern() string { + return r.pathPattern +} + +// Args returns parsed arguments. +func (r Route) Args() []string { + return r.args[:r.count] +} + +// FindRoute finds Route for given method and path. +// +// Note: this method does not unescape path or handle reserved characters in path properly. Use FindPath instead. +func (s *Server) FindRoute(method, path string) (Route, bool) { + return s.FindPath(method, &url.URL{Path: path}) +} + +// FindPath finds Route for given method and URL. +func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { + var ( + elem = u.Path + args = r.args + ) + if rawPath := u.RawPath; rawPath != "" { + if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { + elem = normalized + } + defer func() { + for i, arg := range r.args[:r.count] { + if unescaped, err := url.PathUnescape(arg); err == nil { + r.args[i] = unescaped + } + } + }() + } + + // Static code generated router with unwrapped path search. + switch { + default: + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/pages" + if l := len("/pages"); len(elem) >= l && elem[0:l] == "/pages" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + r.name = "GetPages" + r.operationID = "getPages" + r.pathPattern = "/pages" + r.args = args + r.count = 0 + return r, true + case "POST": + r.name = "AddPage" + r.operationID = "addPage" + r.pathPattern = "/pages" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "id" + // Leaf parameter + args[0] = elem + elem = "" + + if len(elem) == 0 { + switch method { + case "GET": + // Leaf: GetPage + r.name = "GetPage" + r.operationID = "getPage" + r.pathPattern = "/pages/{id}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + } + } + } + return r, false +} diff --git a/api/openapi/oas_schemas_gen.go b/api/openapi/oas_schemas_gen.go new file mode 100644 index 0000000..2cda604 --- /dev/null +++ b/api/openapi/oas_schemas_gen.go @@ -0,0 +1,516 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "fmt" + "time" + + "github.com/go-faster/errors" + "github.com/google/uuid" +) + +func (s *ErrorStatusCode) Error() string { + return fmt.Sprintf("code %d: %+v", s.StatusCode, s.Response) +} + +type AddPageReq struct { + URL string `json:"url"` + Description OptString `json:"description"` + Formats []Format `json:"formats"` +} + +// GetURL returns the value of URL. +func (s *AddPageReq) GetURL() string { + return s.URL +} + +// GetDescription returns the value of Description. +func (s *AddPageReq) GetDescription() OptString { + return s.Description +} + +// GetFormats returns the value of Formats. +func (s *AddPageReq) GetFormats() []Format { + return s.Formats +} + +// SetURL sets the value of URL. +func (s *AddPageReq) SetURL(val string) { + s.URL = val +} + +// SetDescription sets the value of Description. +func (s *AddPageReq) SetDescription(val OptString) { + s.Description = val +} + +// SetFormats sets the value of Formats. +func (s *AddPageReq) SetFormats(val []Format) { + s.Formats = val +} + +// Ref: #/components/schemas/error +type Error struct { + Message string `json:"message"` + Localized OptString `json:"localized"` +} + +// GetMessage returns the value of Message. +func (s *Error) GetMessage() string { + return s.Message +} + +// GetLocalized returns the value of Localized. +func (s *Error) GetLocalized() OptString { + return s.Localized +} + +// SetMessage sets the value of Message. +func (s *Error) SetMessage(val string) { + s.Message = val +} + +// SetLocalized sets the value of Localized. +func (s *Error) SetLocalized(val OptString) { + s.Localized = val +} + +// ErrorStatusCode wraps Error with StatusCode. +type ErrorStatusCode struct { + StatusCode int + Response Error +} + +// GetStatusCode returns the value of StatusCode. +func (s *ErrorStatusCode) GetStatusCode() int { + return s.StatusCode +} + +// GetResponse returns the value of Response. +func (s *ErrorStatusCode) GetResponse() Error { + return s.Response +} + +// SetStatusCode sets the value of StatusCode. +func (s *ErrorStatusCode) SetStatusCode(val int) { + s.StatusCode = val +} + +// SetResponse sets the value of Response. +func (s *ErrorStatusCode) SetResponse(val Error) { + s.Response = val +} + +// Ref: #/components/schemas/format +type Format string + +const ( + FormatAll Format = "all" + FormatPdf Format = "pdf" + FormatSinglePage Format = "single_page" + FormatHeaders Format = "headers" +) + +// MarshalText implements encoding.TextMarshaler. +func (s Format) MarshalText() ([]byte, error) { + switch s { + case FormatAll: + return []byte(s), nil + case FormatPdf: + return []byte(s), nil + case FormatSinglePage: + return []byte(s), nil + case FormatHeaders: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *Format) UnmarshalText(data []byte) error { + switch Format(data) { + case FormatAll: + *s = FormatAll + return nil + case FormatPdf: + *s = FormatPdf + return nil + case FormatSinglePage: + *s = FormatSinglePage + return nil + case FormatHeaders: + *s = FormatHeaders + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + +// GetPageNotFound is response for GetPage operation. +type GetPageNotFound struct{} + +func (*GetPageNotFound) getPageRes() {} + +// NewOptAddPageReq returns new OptAddPageReq with value set to v. +func NewOptAddPageReq(v AddPageReq) OptAddPageReq { + return OptAddPageReq{ + Value: v, + Set: true, + } +} + +// OptAddPageReq is optional AddPageReq. +type OptAddPageReq struct { + Value AddPageReq + Set bool +} + +// IsSet returns true if OptAddPageReq was set. +func (o OptAddPageReq) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptAddPageReq) Reset() { + var v AddPageReq + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptAddPageReq) SetTo(v AddPageReq) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptAddPageReq) Get() (v AddPageReq, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptAddPageReq) Or(d AddPageReq) AddPageReq { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptString returns new OptString with value set to v. +func NewOptString(v string) OptString { + return OptString{ + Value: v, + Set: true, + } +} + +// OptString is optional string. +type OptString struct { + Value string + Set bool +} + +// IsSet returns true if OptString was set. +func (o OptString) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptString) Reset() { + var v string + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptString) SetTo(v string) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptString) Get() (v string, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptString) Or(d string) string { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// Ref: #/components/schemas/page +type Page struct { + ID uuid.UUID `json:"id"` + URL string `json:"url"` + Created time.Time `json:"created"` + Formats []Format `json:"formats"` + Status Status `json:"status"` +} + +// GetID returns the value of ID. +func (s *Page) GetID() uuid.UUID { + return s.ID +} + +// GetURL returns the value of URL. +func (s *Page) GetURL() string { + return s.URL +} + +// GetCreated returns the value of Created. +func (s *Page) GetCreated() time.Time { + return s.Created +} + +// GetFormats returns the value of Formats. +func (s *Page) GetFormats() []Format { + return s.Formats +} + +// GetStatus returns the value of Status. +func (s *Page) GetStatus() Status { + return s.Status +} + +// SetID sets the value of ID. +func (s *Page) SetID(val uuid.UUID) { + s.ID = val +} + +// SetURL sets the value of URL. +func (s *Page) SetURL(val string) { + s.URL = val +} + +// SetCreated sets the value of Created. +func (s *Page) SetCreated(val time.Time) { + s.Created = val +} + +// SetFormats sets the value of Formats. +func (s *Page) SetFormats(val []Format) { + s.Formats = val +} + +// SetStatus sets the value of Status. +func (s *Page) SetStatus(val Status) { + s.Status = val +} + +// Merged schema. +// Ref: #/components/schemas/pageWithResults +type PageWithResults struct { + ID uuid.UUID `json:"id"` + URL string `json:"url"` + Created time.Time `json:"created"` + Formats []Format `json:"formats"` + Status Status `json:"status"` + Results []Result `json:"results"` +} + +// GetID returns the value of ID. +func (s *PageWithResults) GetID() uuid.UUID { + return s.ID +} + +// GetURL returns the value of URL. +func (s *PageWithResults) GetURL() string { + return s.URL +} + +// GetCreated returns the value of Created. +func (s *PageWithResults) GetCreated() time.Time { + return s.Created +} + +// GetFormats returns the value of Formats. +func (s *PageWithResults) GetFormats() []Format { + return s.Formats +} + +// GetStatus returns the value of Status. +func (s *PageWithResults) GetStatus() Status { + return s.Status +} + +// GetResults returns the value of Results. +func (s *PageWithResults) GetResults() []Result { + return s.Results +} + +// SetID sets the value of ID. +func (s *PageWithResults) SetID(val uuid.UUID) { + s.ID = val +} + +// SetURL sets the value of URL. +func (s *PageWithResults) SetURL(val string) { + s.URL = val +} + +// SetCreated sets the value of Created. +func (s *PageWithResults) SetCreated(val time.Time) { + s.Created = val +} + +// SetFormats sets the value of Formats. +func (s *PageWithResults) SetFormats(val []Format) { + s.Formats = val +} + +// SetStatus sets the value of Status. +func (s *PageWithResults) SetStatus(val Status) { + s.Status = val +} + +// SetResults sets the value of Results. +func (s *PageWithResults) SetResults(val []Result) { + s.Results = val +} + +func (*PageWithResults) getPageRes() {} + +type Pages []Page + +// Ref: #/components/schemas/result +type Result struct { + Format Format `json:"format"` + Error OptString `json:"error"` + Files []ResultFilesItem `json:"files"` +} + +// GetFormat returns the value of Format. +func (s *Result) GetFormat() Format { + return s.Format +} + +// GetError returns the value of Error. +func (s *Result) GetError() OptString { + return s.Error +} + +// GetFiles returns the value of Files. +func (s *Result) GetFiles() []ResultFilesItem { + return s.Files +} + +// SetFormat sets the value of Format. +func (s *Result) SetFormat(val Format) { + s.Format = val +} + +// SetError sets the value of Error. +func (s *Result) SetError(val OptString) { + s.Error = val +} + +// SetFiles sets the value of Files. +func (s *Result) SetFiles(val []ResultFilesItem) { + s.Files = val +} + +type ResultFilesItem struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Mimetype string `json:"mimetype"` + Size int64 `json:"size"` +} + +// GetID returns the value of ID. +func (s *ResultFilesItem) GetID() uuid.UUID { + return s.ID +} + +// GetName returns the value of Name. +func (s *ResultFilesItem) GetName() string { + return s.Name +} + +// GetMimetype returns the value of Mimetype. +func (s *ResultFilesItem) GetMimetype() string { + return s.Mimetype +} + +// GetSize returns the value of Size. +func (s *ResultFilesItem) GetSize() int64 { + return s.Size +} + +// SetID sets the value of ID. +func (s *ResultFilesItem) SetID(val uuid.UUID) { + s.ID = val +} + +// SetName sets the value of Name. +func (s *ResultFilesItem) SetName(val string) { + s.Name = val +} + +// SetMimetype sets the value of Mimetype. +func (s *ResultFilesItem) SetMimetype(val string) { + s.Mimetype = val +} + +// SetSize sets the value of Size. +func (s *ResultFilesItem) SetSize(val int64) { + s.Size = val +} + +// Ref: #/components/schemas/status +type Status string + +const ( + StatusNew Status = "new" + StatusProcessing Status = "processing" + StatusDone Status = "done" + StatusFailed Status = "failed" + StatusWithErrors Status = "with_errors" +) + +// MarshalText implements encoding.TextMarshaler. +func (s Status) MarshalText() ([]byte, error) { + switch s { + case StatusNew: + return []byte(s), nil + case StatusProcessing: + return []byte(s), nil + case StatusDone: + return []byte(s), nil + case StatusFailed: + return []byte(s), nil + case StatusWithErrors: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *Status) UnmarshalText(data []byte) error { + switch Status(data) { + case StatusNew: + *s = StatusNew + return nil + case StatusProcessing: + *s = StatusProcessing + return nil + case StatusDone: + *s = StatusDone + return nil + case StatusFailed: + *s = StatusFailed + return nil + case StatusWithErrors: + *s = StatusWithErrors + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} diff --git a/api/openapi/oas_server_gen.go b/api/openapi/oas_server_gen.go new file mode 100644 index 0000000..646ed6f --- /dev/null +++ b/api/openapi/oas_server_gen.go @@ -0,0 +1,52 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "context" +) + +// Handler handles operations described by OpenAPI v3 specification. +type Handler interface { + // AddPage implements addPage operation. + // + // Add new page. + // + // POST /pages + AddPage(ctx context.Context, req OptAddPageReq) (*Page, error) + // GetPage implements getPage operation. + // + // Get page details. + // + // GET /pages/{id} + GetPage(ctx context.Context, params GetPageParams) (GetPageRes, error) + // GetPages implements getPages operation. + // + // Get all pages. + // + // GET /pages + GetPages(ctx context.Context) (Pages, error) + // NewError creates *ErrorStatusCode from error returned by handler. + // + // Used for common default response. + NewError(ctx context.Context, err error) *ErrorStatusCode +} + +// Server implements http server based on OpenAPI v3 specification and +// calls Handler to handle requests. +type Server struct { + h Handler + baseServer +} + +// NewServer creates new Server. +func NewServer(h Handler, opts ...ServerOption) (*Server, error) { + s, err := newServerConfig(opts...).baseServer() + if err != nil { + return nil, err + } + return &Server{ + h: h, + baseServer: s, + }, nil +} diff --git a/api/openapi/oas_unimplemented_gen.go b/api/openapi/oas_unimplemented_gen.go new file mode 100644 index 0000000..03c52e6 --- /dev/null +++ b/api/openapi/oas_unimplemented_gen.go @@ -0,0 +1,49 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "context" + + ht "github.com/ogen-go/ogen/http" +) + +// UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented. +type UnimplementedHandler struct{} + +var _ Handler = UnimplementedHandler{} + +// AddPage implements addPage operation. +// +// Add new page. +// +// POST /pages +func (UnimplementedHandler) AddPage(ctx context.Context, req OptAddPageReq) (r *Page, _ error) { + return r, ht.ErrNotImplemented +} + +// GetPage implements getPage operation. +// +// Get page details. +// +// GET /pages/{id} +func (UnimplementedHandler) GetPage(ctx context.Context, params GetPageParams) (r GetPageRes, _ error) { + return r, ht.ErrNotImplemented +} + +// GetPages implements getPages operation. +// +// Get all pages. +// +// GET /pages +func (UnimplementedHandler) GetPages(ctx context.Context) (r Pages, _ error) { + return r, ht.ErrNotImplemented +} + +// NewError creates *ErrorStatusCode from error returned by handler. +// +// Used for common default response. +func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *ErrorStatusCode) { + r = new(ErrorStatusCode) + return r +} diff --git a/api/openapi/oas_validators_gen.go b/api/openapi/oas_validators_gen.go new file mode 100644 index 0000000..bbc15c1 --- /dev/null +++ b/api/openapi/oas_validators_gen.go @@ -0,0 +1,247 @@ +// Code generated by ogen, DO NOT EDIT. + +package openapi + +import ( + "fmt" + + "github.com/go-faster/errors" + + "github.com/ogen-go/ogen/validate" +) + +func (s *AddPageReq) Validate() error { + var failures []validate.FieldError + if err := func() error { + var failures []validate.FieldError + for i, elem := range s.Formats { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "formats", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} +func (s Format) Validate() error { + switch s { + case "all": + return nil + case "pdf": + return nil + case "single_page": + return nil + case "headers": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + +func (s *Page) Validate() error { + var failures []validate.FieldError + if err := func() error { + if s.Formats == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Formats { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "formats", + Error: err, + }) + } + if err := func() error { + if err := s.Status.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "status", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} +func (s *PageWithResults) Validate() error { + var failures []validate.FieldError + if err := func() error { + if s.Formats == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Formats { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "formats", + Error: err, + }) + } + if err := func() error { + if err := s.Status.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "status", + Error: err, + }) + } + if err := func() error { + if s.Results == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Results { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "results", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} +func (s Pages) Validate() error { + if s == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} +func (s *Result) Validate() error { + var failures []validate.FieldError + if err := func() error { + if err := s.Format.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "format", + Error: err, + }) + } + if err := func() error { + if s.Files == nil { + return errors.New("nil is invalid value") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "files", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} +func (s Status) Validate() error { + switch s { + case "new": + return nil + case "processing": + return nil + case "done": + return nil + case "failed": + return nil + case "with_errors": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} diff --git a/application/application.go b/application/application.go new file mode 100644 index 0000000..27d51f4 --- /dev/null +++ b/application/application.go @@ -0,0 +1,182 @@ +package application + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "sync" + "time" + + "github.com/dgraph-io/badger/v4" + "github.com/ogen-go/ogen/middleware" + "go.uber.org/multierr" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/derfenix/webarchive/adapters/processors" + badgerRepo "github.com/derfenix/webarchive/adapters/repository/badger" + "github.com/derfenix/webarchive/api/openapi" + "github.com/derfenix/webarchive/entity" + "github.com/derfenix/webarchive/ports/rest" +) + +func NewApplication(cfg Config) (Application, error) { + log, err := newLogger(cfg.Logging) + if err != nil { + return Application{}, fmt.Errorf("new logger: %w", err) + } + + db, err := badgerRepo.NewBadger(cfg.DB.Path, log.Named("db")) + if err != nil { + return Application{}, fmt.Errorf("new badger: %w", err) + } + + fileRepo := badgerRepo.NewFile(db) + pageRepo, err := badgerRepo.NewPage(db, fileRepo) + if err != nil { + return Application{}, fmt.Errorf("new page repo: %w", err) + } + + processor, err := processors.NewProcessors() + if err != nil { + return Application{}, fmt.Errorf("new processors: %w", err) + } + + server, err := openapi.NewServer( + rest.NewService(pageRepo), + openapi.WithMiddleware( + func(r middleware.Request, next middleware.Next) (middleware.Response, error) { + start := time.Now() + + log := log.With( + zap.String("operation_id", r.OperationID), + zap.String("uri", r.Raw.RequestURI), + ) + + var response middleware.Response + var reqErr error + + response, reqErr = next(r) + + log.Debug("request completed", zap.Duration("duration", time.Since(start)), zap.Error(err)) + + return response, reqErr + }, + ), + ) + if err != nil { + return Application{}, fmt.Errorf("new rest server: %w", err) + } + + httpServer := http.Server{ + Addr: "0.0.0.0:5001", + Handler: server, + ReadTimeout: time.Second * 15, + ReadHeaderTimeout: time.Second * 5, + IdleTimeout: time.Second * 30, + MaxHeaderBytes: 1024 * 2, + } + + return Application{ + cfg: cfg, + log: log, + db: db, + processor: processor, + httpServer: &httpServer, + + pageRepo: pageRepo, + fileRepo: fileRepo, + }, nil +} + +type Application struct { + cfg Config + log *zap.Logger + db *badger.DB + processor entity.Processor + + httpServer *http.Server + + pageRepo *badgerRepo.Page + fileRepo *badgerRepo.File +} + +func (a *Application) Log() *zap.Logger { + return a.log +} + +func (a *Application) Start(ctx context.Context, wg *sync.WaitGroup) error { + wg.Add(2) + + a.httpServer.BaseContext = func(net.Listener) context.Context { + return ctx + } + + go func() { + defer wg.Done() + + <-ctx.Done() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + if err := a.httpServer.Shutdown(shutdownCtx); err != nil { + a.log.Warn("http graceful shutdown failed", zap.Error(err)) + } + }() + + go func() { + defer wg.Done() + + a.log.Info("starting http server", zap.String("address", a.httpServer.Addr)) + + if err := a.httpServer.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + a.log.Error("http serve error", zap.Error(err)) + } + + a.log.Info("http server stopped") + } + }() + + return nil +} + +func (a *Application) Stop() error { + var errs error + + if err := a.db.Sync(); err != nil { + errs = multierr.Append(errs, fmt.Errorf("sync db: %w", err)) + } + + if err := badgerRepo.Backup(a.db, badgerRepo.BackupStop); err != nil { + errs = multierr.Append(errs, fmt.Errorf("backup on stop: %w", err)) + } + + if err := a.db.Close(); err != nil { + errs = multierr.Append(errs, fmt.Errorf("close db: %w", err)) + } + + return errs +} + +func newLogger(cfg Logging) (*zap.Logger, error) { + logCfg := zap.NewProductionConfig() + logCfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder + logCfg.EncoderConfig.EncodeDuration = zapcore.NanosDurationEncoder + logCfg.DisableCaller = true + + logCfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) + if cfg.Debug { + logCfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) + } + + log, err := logCfg.Build() + if err != nil { + return nil, fmt.Errorf("build logger: %w", err) + } + + return log, nil +} diff --git a/application/config.go b/application/config.go new file mode 100644 index 0000000..90bdf13 --- /dev/null +++ b/application/config.go @@ -0,0 +1,38 @@ +package application + +import ( + "context" + "fmt" + + "github.com/sethvargo/go-envconfig" +) + +const envPrefix = "WEBARCHIVE_" + +func NewConfig(ctx context.Context) (Config, error) { + cfg := Config{} + + lookuper := envconfig.MultiLookuper( + envconfig.PrefixLookuper(envPrefix, envconfig.OsLookuper()), + envconfig.OsLookuper(), + ) + + if err := envconfig.ProcessWith(ctx, &cfg, lookuper); err != nil { + return Config{}, fmt.Errorf("process env: %w", err) + } + + return cfg, nil +} + +type Config struct { + DB DB `env:",prefix=DB_"` + Logging Logging `env:",prefix=LOGGING_"` +} + +type DB struct { + Path string `env:"PATH,default=./db"` +} + +type Logging struct { + Debug bool `env:"DEBUG"` +} diff --git a/application/config_test.go b/application/config_test.go new file mode 100644 index 0000000..6aa4af8 --- /dev/null +++ b/application/config_test.go @@ -0,0 +1,42 @@ +package application + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("no envs", func(t *testing.T) { + + config, err := NewConfig(ctx) + require.NoError(t, err) + + assert.Equal(t, "./db", config.DB.Path) + }) + + t.Run("env without prefix", func(t *testing.T) { + require.NoError(t, os.Setenv("DB_PATH", "./old_db")) + + config, err := NewConfig(ctx) + require.NoError(t, err) + + assert.Equal(t, "./old_db", config.DB.Path) + }) + + t.Run("prefix env override", func(t *testing.T) { + require.NoError(t, os.Setenv("WEBARCHIVE_DB_PATH", "./new_db")) + + config, err := NewConfig(ctx) + require.NoError(t, err) + + assert.Equal(t, "./new_db", config.DB.Path) + }) +} diff --git a/cmd/service/main.go b/cmd/service/main.go new file mode 100644 index 0000000..acecf8b --- /dev/null +++ b/cmd/service/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + + "go.uber.org/zap" + + "github.com/derfenix/webarchive/application" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer cancel() + + cfg, err := application.NewConfig(ctx) + if err != nil { + fmt.Printf("failed to init config: %s", err.Error()) + os.Exit(2) + } + + app, err := application.NewApplication(cfg) + if err != nil { + fmt.Printf("failed to init application: %s", err.Error()) + os.Exit(2) + } + + wg := sync.WaitGroup{} + + if err := app.Start(ctx, &wg); err != nil { + app.Log().Fatal("failed to start application", zap.Error(err)) + } + + wg.Wait() + + if err := app.Stop(); err != nil { + app.Log().Fatal("failed to graceful stop", zap.Error(err)) + } +} diff --git a/entity/file.go b/entity/file.go new file mode 100644 index 0000000..2b8613b --- /dev/null +++ b/entity/file.go @@ -0,0 +1,30 @@ +package entity + +import ( + "time" + + "github.com/gabriel-vasile/mimetype" + "github.com/google/uuid" +) + +func NewFile(name string, data []byte) File { + detected := mimetype.Detect(data) + + return File{ + ID: uuid.New(), + Name: name, + MimeType: detected.String(), + Size: int64(len(data)), + Data: data, + Created: time.Now(), + } +} + +type File struct { + ID uuid.UUID + Name string + MimeType string + Size int64 + Data []byte + Created time.Time +} diff --git a/entity/page.go b/entity/page.go new file mode 100644 index 0000000..fc86cad --- /dev/null +++ b/entity/page.go @@ -0,0 +1,99 @@ +package entity + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/google/uuid" +) + +type Processor interface { + Process(ctx context.Context, format Format, url string) Result +} + +type Format uint8 + +const ( + FormatHeaders Format = iota + FormatSingleFile + FormatPDF +) + +type Status uint8 + +const ( + StatusNew Status = iota + StatusProcessing + StatusDone + StatusFailed + StatusWithErrors +) + +func NewPage(url string, description string, formats ...Format) *Page { + return &Page{ + ID: uuid.New(), + URL: url, + Description: description, + Formats: formats, + Created: time.Now(), + Version: 1, + } +} + +type Page struct { + ID uuid.UUID + URL string + Description string + Created time.Time + Formats []Format + Results Results + Version uint16 + Status Status +} + +func (p *Page) SetProcessing() { + p.Status = StatusProcessing +} + +func (p *Page) Process(ctx context.Context, wg *sync.WaitGroup, processor Processor) { + defer wg.Done() + + innerWG := sync.WaitGroup{} + innerWG.Add(len(p.Formats)) + + for _, format := range p.Formats { + go func(format Format) { + defer innerWG.Done() + + defer func() { + if err := recover(); err != nil { + p.Results.Add(Result{Format: format, Err: fmt.Errorf("recovered from panic: %v", err)}) + } + }() + + result := processor.Process(ctx, format, p.URL) + p.Results.Add(result) + }(format) + } + + var hasResultWithOutErrors bool + for _, result := range p.Results.Results() { + if result.Err != nil { + p.Status = StatusWithErrors + } else { + hasResultWithOutErrors = true + } + } + + if !hasResultWithOutErrors { + p.Status = StatusFailed + } + + if p.Status == StatusProcessing { + p.Status = StatusDone + } + + innerWG.Wait() +} diff --git a/entity/result.go b/entity/result.go new file mode 100644 index 0000000..f2bb9f9 --- /dev/null +++ b/entity/result.go @@ -0,0 +1,39 @@ +package entity + +import ( + "sync" + + "github.com/vmihailenco/msgpack/v5" +) + +type Result struct { + Format Format + Err error + Files []File +} + +type Results struct { + mu sync.RWMutex + results []Result +} + +func (r *Results) MarshalMsgpack() ([]byte, error) { + return msgpack.Marshal(r.results) +} + +func (r *Results) UnmarshalMsgpack(b []byte) error { + return msgpack.Unmarshal(b, r.results) +} + +func (r *Results) Add(result Result) { + r.mu.Lock() + r.results = append(r.results, result) + r.mu.Unlock() +} + +func (r *Results) Results() []Result { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.results +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2030333 --- /dev/null +++ b/go.mod @@ -0,0 +1,59 @@ +module github.com/derfenix/webarchive + +go 1.20 + +require ( + github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0 + github.com/dgraph-io/badger/v4 v4.0.1 + github.com/gabriel-vasile/mimetype v1.4.2 + github.com/go-faster/errors v0.6.1 + github.com/go-faster/jx v1.0.0 + github.com/google/uuid v1.3.0 + github.com/ogen-go/ogen v0.60.1 + github.com/sethvargo/go-envconfig v0.9.0 + github.com/stretchr/testify v1.8.2 + github.com/vmihailenco/msgpack/v5 v5.3.5 + go.opentelemetry.io/otel v1.14.0 + go.opentelemetry.io/otel/metric v0.37.0 + go.opentelemetry.io/otel/trace v1.14.0 + go.uber.org/multierr v1.10.0 + go.uber.org/zap v1.24.0 +) + +require ( + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dlclark/regexp2 v1.8.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-faster/yamlx v0.4.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.1.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/flatbuffers v23.3.3+incompatible // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dfbc794 --- /dev/null +++ b/go.sum @@ -0,0 +1,231 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0 h1:DNrExYwvyyI404SxdUCCANAj9TwnGjRfa3cYFMNY1AU= +github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0/go.mod h1:SQq4xfIdvf6WYKSDxAJc+xOJdolt+/bc1jnQKMtPMvQ= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.0.1 h1:zwLYFc4sfxKdaRTvS6wlHsSuYWNUiWnYLU+TS+/nCDI= +github.com/dgraph-io/badger/v4 v4.0.1/go.mod h1:edFJfgVfwYjg+grodpS7Yj2vohQMK3VL6eCaR6EpRJU= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= +github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= +github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= +github.com/go-faster/jx v1.0.0 h1:HE+ms2e6ZGkZ6u13t8u+onBinrPvIPI+0hWXGELm74g= +github.com/go-faster/jx v1.0.0/go.mod h1:zm8SlkwK+H0TYNKYtVJ/7cWFS7soJBQWhcPctKyYL/4= +github.com/go-faster/yamlx v0.4.1 h1:00RQkZopoLDF1SgBDJVHuN6epTOK7T0TkN427vbvEBk= +github.com/go-faster/yamlx v0.4.1/go.mod h1:QXr/i3Z00jRhskgyWkoGsEdseebd/ZbZEpGS6DJv8oo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v23.3.3+incompatible h1:5PJI/WbJkaMTvpGxsHVKG/LurN/KnWXNyGpwSCDgen0= +github.com/google/flatbuffers v23.3.3+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/ogen-go/ogen v0.60.1 h1:yOt0i6NcH7jM3rBi9nnv5VsGUQRw4ACUMsiJojnqrAM= +github.com/ogen-go/ogen v0.60.1/go.mod h1:tcwLpHe4vyk9xtbTMe3yu3Qtcbz8VjrpBz9LzsdwWvQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= +github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/metric v0.37.0 h1:pHDQuLQOZwYD+Km0eb657A25NaRzy0a+eLyKfDXedEs= +go.opentelemetry.io/otel/metric v0.37.0/go.mod h1:DmdaHfGt54iV6UKxsV9slj2bBRJcKC1B1uvDLIioc1s= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/ports/rest/converter.go b/ports/rest/converter.go new file mode 100644 index 0000000..e2252b6 --- /dev/null +++ b/ports/rest/converter.go @@ -0,0 +1,132 @@ +package rest + +import ( + "github.com/derfenix/webarchive/api/openapi" + "github.com/derfenix/webarchive/entity" +) + +func PageToRestWithResults(page *entity.Page) openapi.PageWithResults { + return openapi.PageWithResults{ + ID: page.ID, + URL: page.URL, + Created: page.Created, + Formats: func() []openapi.Format { + res := make([]openapi.Format, len(page.Formats)) + + for i, format := range page.Formats { + res[i] = FormatToRest(format) + } + + return res + }(), + Status: StatusToRest(page.Status), + Results: func() []openapi.Result { + results := make([]openapi.Result, len(page.Results.Results())) + + for i := range results { + result := &page.Results.Results()[i] + + results[i] = openapi.Result{ + Format: FormatToRest(result.Format), + Error: openapi.NewOptString(result.Err.Error()), + Files: func() []openapi.ResultFilesItem { + files := make([]openapi.ResultFilesItem, len(results[i].Files)) + + for j := range files { + file := &result.Files[j] + + files[i] = openapi.ResultFilesItem{ + ID: file.ID, + Name: file.Name, + Mimetype: file.MimeType, + Size: file.Size, + } + } + + return files + }(), + } + } + + return results + }(), + } +} + +func PageToRest(page *entity.Page) openapi.Page { + return openapi.Page{ + ID: page.ID, + URL: page.URL, + Created: page.Created, + Formats: func() []openapi.Format { + res := make([]openapi.Format, len(page.Formats)) + + for i, format := range page.Formats { + res[i] = FormatToRest(format) + } + + return res + }(), + Status: StatusToRest(page.Status), + } +} + +func StatusToRest(s entity.Status) openapi.Status { + switch s { + case entity.StatusNew: + return openapi.StatusNew + case entity.StatusProcessing: + return openapi.StatusProcessing + case entity.StatusDone: + return openapi.StatusDone + case entity.StatusFailed: + return openapi.StatusFailed + case entity.StatusWithErrors: + return openapi.StatusWithErrors + default: + return "" + } +} + +func FormatFromRest(format []openapi.Format) []entity.Format { + var formats []entity.Format + + switch { + case len(format) == 0 || (len(format) == 1 && format[0] == openapi.FormatAll): + formats = []entity.Format{ + entity.FormatHeaders, + entity.FormatPDF, + entity.FormatSingleFile, + } + + default: + formats = make([]entity.Format, len(format)) + for i, format := range format { + switch format { + case openapi.FormatPdf: + formats[i] = entity.FormatPDF + + case openapi.FormatHeaders: + formats[i] = entity.FormatHeaders + + case openapi.FormatSinglePage: + formats[i] = entity.FormatSingleFile + } + } + } + + return formats +} + +func FormatToRest(format entity.Format) openapi.Format { + switch format { + case entity.FormatPDF: + return openapi.FormatPdf + case entity.FormatSingleFile: + return openapi.FormatSinglePage + case entity.FormatHeaders: + return openapi.FormatHeaders + default: + return "" + } +} diff --git a/ports/rest/service.go b/ports/rest/service.go new file mode 100644 index 0000000..c45f518 --- /dev/null +++ b/ports/rest/service.go @@ -0,0 +1,75 @@ +package rest + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/uuid" + + "github.com/derfenix/webarchive/api/openapi" + "github.com/derfenix/webarchive/entity" +) + +type Pages interface { + ListAll(ctx context.Context) ([]*entity.Page, error) + Save(ctx context.Context, site *entity.Page) error + Get(_ context.Context, id uuid.UUID) (*entity.Page, error) +} + +func NewService(sites Pages) *Service { + return &Service{pages: sites} +} + +type Service struct { + openapi.UnimplementedHandler + pages Pages +} + +func (s *Service) GetPage(ctx context.Context, params openapi.GetPageParams) (openapi.GetPageRes, error) { + page, err := s.pages.Get(ctx, params.ID) + if err != nil { + return &openapi.GetPageNotFound{}, nil + } + + restPage := PageToRestWithResults(page) + + return &restPage, nil +} + +func (s *Service) AddPage(ctx context.Context, req openapi.OptAddPageReq) (*openapi.Page, error) { + site := entity.NewPage(req.Value.URL, req.Value.Description.Value, FormatFromRest(req.Value.Formats)...) + + err := s.pages.Save(ctx, site) + if err != nil { + return nil, fmt.Errorf("save site: %w", err) + } + + res := PageToRest(site) + + return &res, nil +} + +func (s *Service) GetPages(ctx context.Context) (openapi.Pages, error) { + sites, err := s.pages.ListAll(ctx) + if err != nil { + return nil, fmt.Errorf("list all: %w", err) + } + + res := make(openapi.Pages, len(sites)) + for i := range res { + res[i] = PageToRest(sites[i]) + } + + return res, nil +} + +func (s *Service) NewError(_ context.Context, err error) *openapi.ErrorStatusCode { + return &openapi.ErrorStatusCode{ + StatusCode: http.StatusInternalServerError, + Response: openapi.Error{ + Message: err.Error(), + Localized: openapi.OptString{}, + }, + } +}