Compare commits
15 Commits
88ab880a8c
...
1fed2d648f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fed2d648f | ||
|
|
a372df64a5 | ||
|
|
ab396d1833 | ||
|
|
a097814a62 | ||
|
|
18fd8dfac5 | ||
|
|
ce02422b1d | ||
|
|
ec1f0e007a | ||
|
|
19b6d180e7 | ||
|
|
e54c7a76f9 | ||
|
|
537eaf9801 | ||
|
|
baa97ab62d | ||
|
|
f57408d003 | ||
|
|
05587ac28e | ||
|
|
7f4f760407 | ||
|
|
7f97eecbca |
1
go.mod
1
go.mod
@@ -10,6 +10,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
modernc.org/libc v1.62.1 // indirect
|
modernc.org/libc v1.62.1 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -10,6 +10,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
|||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package cron
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"fmt"
|
||||||
"show-rss/src/feeds"
|
"show-rss/src/feeds"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -24,11 +24,24 @@ func Main(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func One(ctx context.Context) error {
|
func One(ctx context.Context) error {
|
||||||
feeds, err := feeds.New(ctx)
|
return feeds.ForEach(ctx, func(feed feeds.Feed) error {
|
||||||
if err != nil {
|
if err := one(ctx, feed); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to cron %s (%+v): %w", feed.Entry.ID, feed.Version, err)
|
||||||
}
|
}
|
||||||
_ = feeds
|
return nil
|
||||||
|
})
|
||||||
return io.EOF
|
}
|
||||||
|
|
||||||
|
func one(ctx context.Context, feed feeds.Feed) error {
|
||||||
|
if should, err := feed.ShouldExecute(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !should {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("should GET %s", feed.Version.URL)
|
||||||
|
|
||||||
|
return fmt.Errorf("should parse %s", feed.Version.URL)
|
||||||
|
|
||||||
|
return feed.Executed(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package cron_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"show-rss/src/cmd/cron"
|
"show-rss/src/cmd/cron"
|
||||||
"show-rss/src/db"
|
"show-rss/src/db"
|
||||||
|
"show-rss/src/feeds"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,24 +18,58 @@ func TestOne(t *testing.T) {
|
|||||||
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer can()
|
defer can()
|
||||||
|
|
||||||
t.Run("same ctx", func(t *testing.T) {
|
for name, aCtx := range map[string]func() context.Context{
|
||||||
ctx := db.Test(t, ctx)
|
"empty": func() context.Context {
|
||||||
for i := 0; i < 2; i++ {
|
return db.Test(t, ctx)
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
},
|
||||||
if err := cron.One(ctx); err != nil && ctx.Err() == nil {
|
"feeds": func() context.Context {
|
||||||
t.Fatalf("failed %d: %v", i, err)
|
gets := []string{}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gets = append(gets, r.URL.String())
|
||||||
|
t.Logf("%s", gets[len(gets)-1])
|
||||||
|
}))
|
||||||
|
t.Cleanup(s.Close)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
slices.Sort(gets)
|
||||||
|
if len(gets) != 2+2+2 { // id=1+id=2 for each of 2 unrecycled ctx, id=1+id=2 for one across shared ctx
|
||||||
|
t.Errorf("didn't call urls exactly twice: %+v", gets)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("new ctx", func(t *testing.T) {
|
ctx := db.Test(t, ctx)
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
for i := 0; i < 2; i++ {
|
||||||
if err := cron.One(db.Test(t, ctx)); err != nil && ctx.Err() == nil {
|
if _, err := feeds.Insert(ctx, fmt.Sprintf("%s?idx=%d", s.URL, i), "* * * * *", "matches"); err != nil {
|
||||||
t.Fatalf("failed %d: %v", i, err)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
name := name
|
||||||
|
aCtx := aCtx
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Run("same ctx", func(t *testing.T) {
|
||||||
|
ctx := aCtx()
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
|
if err := cron.One(ctx); err != nil && ctx.Err() == nil {
|
||||||
|
t.Fatalf("failed %d: %v", i, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
t.Run("new ctx", func(t *testing.T) {
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
|
ctx := aCtx()
|
||||||
|
if err := cron.One(ctx); err != nil && ctx.Err() == nil {
|
||||||
|
t.Fatalf("failed %d: %v", i, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
396
src/feeds/db.go
396
src/feeds/db.go
@@ -11,10 +11,234 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Feeds struct{}
|
type (
|
||||||
|
Feed struct {
|
||||||
|
Entry Entry
|
||||||
|
Version Version
|
||||||
|
Execution Execution
|
||||||
|
}
|
||||||
|
|
||||||
func New(ctx context.Context) (Feeds, error) {
|
Entry struct {
|
||||||
return Feeds{}, initDB(ctx)
|
ID string
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
Deleted time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
Version struct {
|
||||||
|
Created time.Time
|
||||||
|
URL string
|
||||||
|
Cron string
|
||||||
|
Pattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
Execution struct {
|
||||||
|
Executed time.Time
|
||||||
|
Version time.Time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ForEach(ctx context.Context, cb func(Feed) error) error {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type id struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
ids, err := db.Query[id](ctx, `SELECT id FROM "feed.entries" WHERE deleted_at IS NULL`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
feed, err := Get(ctx, id.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if err := cb(feed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(ctx context.Context, id string) (Feed, error) {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return Feed{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.QueryOne[Feed](ctx, `
|
||||||
|
WITH
|
||||||
|
entry AS (
|
||||||
|
SELECT
|
||||||
|
id AS ID,
|
||||||
|
created_at AS Created,
|
||||||
|
updated_at AS Updated,
|
||||||
|
deleted_at AS Deleted
|
||||||
|
FROM "feed.entries"
|
||||||
|
WHERE id = ?
|
||||||
|
),
|
||||||
|
execution AS (
|
||||||
|
SELECT
|
||||||
|
executed_at AS Executed,
|
||||||
|
versions_created_at AS Version
|
||||||
|
FROM "feed.executions"
|
||||||
|
WHERE entries_id = ?
|
||||||
|
ORDER BY executed DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
entry.ID AS "Entry.ID",
|
||||||
|
entry.Created AS "Entry.Created",
|
||||||
|
entry.Updated AS "Entry.Updated",
|
||||||
|
entry.Deleted AS "Entry.Deleted",
|
||||||
|
versions.created_at AS "Version.Created",
|
||||||
|
versions.url AS "Version.URL",
|
||||||
|
versions.cron AS "Version.Cron",
|
||||||
|
versions.pattern AS "Version.Pattern",
|
||||||
|
(
|
||||||
|
SELECT executed_at
|
||||||
|
FROM "feed.executions"
|
||||||
|
WHERE entries_id = entry.ID
|
||||||
|
ORDER BY executed_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS "Execution.Executed",
|
||||||
|
(
|
||||||
|
SELECT versions_created_at
|
||||||
|
FROM "feed.executions"
|
||||||
|
WHERE entries_id = entry.ID
|
||||||
|
ORDER BY executed_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS "Execution.Version"
|
||||||
|
FROM entry
|
||||||
|
JOIN "feed.versions" version_entries_id ON
|
||||||
|
version_entries_id.entries_id=entry.ID
|
||||||
|
JOIN "feed.versions" versions ON
|
||||||
|
versions.created_at=entry.Updated
|
||||||
|
`, id, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func oldGet(ctx context.Context, id string) (Feed, error) {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return Feed{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := getEntry(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return Feed{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := db.QueryOne[Version](ctx, `
|
||||||
|
SELECT
|
||||||
|
"feed.current_versions".versions_created_at AS Created,
|
||||||
|
"feed.current_versions".url AS URL,
|
||||||
|
"feed.current_versions".cron AS Cron,
|
||||||
|
"feed.current_versions".pattern AS Pattern
|
||||||
|
FROM
|
||||||
|
"feed.current_versions"
|
||||||
|
JOIN
|
||||||
|
"feed.versions" versions_a ON
|
||||||
|
"feed.current_versions".entries_id=versions_a.entries_id
|
||||||
|
JOIN
|
||||||
|
"feed.versions" versions_b ON
|
||||||
|
"feed.current_versions".versions_created_at=versions_b.created_at
|
||||||
|
WHERE
|
||||||
|
"feed.current_versions".entries_id = ?
|
||||||
|
`, id)
|
||||||
|
if err != nil {
|
||||||
|
return Feed{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
execution, err := db.QueryOne[Execution](ctx, `
|
||||||
|
SELECT
|
||||||
|
"feed.executed_at" AS Executed,
|
||||||
|
"feed.versions_created_at" AS VersionsCreated
|
||||||
|
FROM
|
||||||
|
"feed.executions"
|
||||||
|
WHERE
|
||||||
|
"feed.executions".entries_id = ?
|
||||||
|
ORDER BY "feed.executions".executed_at DESC
|
||||||
|
`, id)
|
||||||
|
if err != nil {
|
||||||
|
return Feed{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Feed{}, fmt.Errorf("%+v, %+v, %+v", entry, version, execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (feed Feed) Executed(ctx context.Context) error {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
id := feed.Entry.ID
|
||||||
|
version := feed.Version.Created
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
return db.Exec(ctx, `
|
||||||
|
INSERT INTO "feed.executions" (
|
||||||
|
entries_id,
|
||||||
|
versions_created_at,
|
||||||
|
executed_at
|
||||||
|
) VALUES (?, ?, ?);
|
||||||
|
`,
|
||||||
|
id, version, now,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Insert(ctx context.Context, url, cron, pattern string) (string, error) {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
id := uuid.New().String()
|
||||||
|
return id, db.Exec(ctx, `
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO "feed.entries" (
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3);
|
||||||
|
INSERT INTO "feed.versions" (
|
||||||
|
entries_id,
|
||||||
|
created_at,
|
||||||
|
url,
|
||||||
|
cron,
|
||||||
|
pattern
|
||||||
|
) VALUES ($4, $5, $6, $7, $8);
|
||||||
|
COMMIT;
|
||||||
|
`,
|
||||||
|
id, now, now,
|
||||||
|
id, now, url, cron, pattern,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (feed Feed) Update(ctx context.Context, url, cron, pattern *string) error {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (feed Feed) Delete(ctx context.Context) error {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEntry(ctx context.Context, id string) (Entry, error) {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return Entry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.QueryOne[Entry](ctx, `
|
||||||
|
SELECT
|
||||||
|
id AS ID,
|
||||||
|
created_at AS Created,
|
||||||
|
updated_at AS Updated,
|
||||||
|
deleted_at AS Deleted
|
||||||
|
FROM
|
||||||
|
"feed.entries"
|
||||||
|
WHERE
|
||||||
|
id = ?
|
||||||
|
`, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDB(ctx context.Context) error {
|
func initDB(ctx context.Context) error {
|
||||||
@@ -51,6 +275,7 @@ func initDB(ctx context.Context) error {
|
|||||||
)`,
|
)`,
|
||||||
`ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`,
|
`ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`,
|
||||||
`ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`,
|
`ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`,
|
||||||
|
`ALTER TABLE "feed.versions" ADD COLUMN pattern TEXT NOT NULL DEFAULT ''`,
|
||||||
|
|
||||||
`CREATE TABLE "feed.executions" (
|
`CREATE TABLE "feed.executions" (
|
||||||
entries_id TEXT,
|
entries_id TEXT,
|
||||||
@@ -72,168 +297,3 @@ func initDB(ctx context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
|
||||||
Feed struct {
|
|
||||||
Entry Entry
|
|
||||||
Version Version
|
|
||||||
Execution Execution
|
|
||||||
}
|
|
||||||
|
|
||||||
Entry struct {
|
|
||||||
ID string
|
|
||||||
Created time.Time
|
|
||||||
Updated time.Time
|
|
||||||
Deleted time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
Version struct {
|
|
||||||
Created time.Time
|
|
||||||
URL string
|
|
||||||
Cron string
|
|
||||||
}
|
|
||||||
|
|
||||||
Execution struct {
|
|
||||||
Executed time.Time
|
|
||||||
Version time.Time
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (f *Feeds) Get(ctx context.Context, id string) (Feed, error) {
|
|
||||||
return db.QueryOne[Feed](ctx, `
|
|
||||||
WITH
|
|
||||||
entry AS (
|
|
||||||
SELECT
|
|
||||||
id AS ID,
|
|
||||||
created_at AS Created,
|
|
||||||
updated_at AS Updated,
|
|
||||||
deleted_at AS Deleted
|
|
||||||
FROM "feed.entries"
|
|
||||||
WHERE id = ?
|
|
||||||
),
|
|
||||||
execution AS (
|
|
||||||
SELECT
|
|
||||||
executed_at AS Executed,
|
|
||||||
versions_created_at AS Version
|
|
||||||
FROM "feed.executions"
|
|
||||||
WHERE entries_id = ?
|
|
||||||
ORDER BY executed DESC
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
entry.ID AS "Entry.ID",
|
|
||||||
entry.Created AS "Entry.Created",
|
|
||||||
entry.Updated AS "Entry.Updated",
|
|
||||||
entry.Deleted AS "Entry.Deleted",
|
|
||||||
versions.created_at AS "Version.Created",
|
|
||||||
versions.url AS "Version.URL",
|
|
||||||
versions.cron AS "Version.Cron",
|
|
||||||
(
|
|
||||||
SELECT executed_at
|
|
||||||
FROM "feed.executions"
|
|
||||||
WHERE entries_id = entry.ID
|
|
||||||
ORDER BY executed_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
) AS "Execution.Executed",
|
|
||||||
(
|
|
||||||
SELECT versions_created_at
|
|
||||||
FROM "feed.executions"
|
|
||||||
WHERE entries_id = entry.ID
|
|
||||||
ORDER BY executed_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
) AS "Execution.Version"
|
|
||||||
FROM entry
|
|
||||||
JOIN "feed.versions" version_entries_id ON
|
|
||||||
version_entries_id.entries_id=entry.ID
|
|
||||||
JOIN "feed.versions" versions ON
|
|
||||||
versions.created_at=entry.Updated
|
|
||||||
`, id, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Feeds) oldGet(ctx context.Context, id string) (Feed, error) {
|
|
||||||
entry, err := f.getEntry(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return Feed{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
version, err := db.QueryOne[Version](ctx, `
|
|
||||||
SELECT
|
|
||||||
"feed.current_versions".versions_created_at AS Created,
|
|
||||||
"feed.current_versions" AS URL,
|
|
||||||
"feed.current_versions" AS Cron
|
|
||||||
FROM
|
|
||||||
"feed.current_versions"
|
|
||||||
JOIN
|
|
||||||
"feed.versions" versions_a ON
|
|
||||||
"feed.current_versions".entries_id=versions_a.entries_id
|
|
||||||
JOIN
|
|
||||||
"feed.versions" versions_b ON
|
|
||||||
"feed.current_versions".versions_created_at=versions_b.created_at
|
|
||||||
WHERE
|
|
||||||
"feed.current_versions".entries_id = ?
|
|
||||||
`, id)
|
|
||||||
if err != nil {
|
|
||||||
return Feed{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
execution, err := db.QueryOne[Execution](ctx, `
|
|
||||||
SELECT
|
|
||||||
"feed.executed_at" AS Executed,
|
|
||||||
"feed.versions_created_at" AS VersionsCreated
|
|
||||||
FROM
|
|
||||||
"feed.executions"
|
|
||||||
WHERE
|
|
||||||
"feed.executions".entries_id = ?
|
|
||||||
ORDER BY "feed.executions".executed_at DESC
|
|
||||||
`, id)
|
|
||||||
if err != nil {
|
|
||||||
return Feed{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Feed{}, fmt.Errorf("%+v, %+v, %+v", entry, version, execution)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Feeds) Insert(ctx context.Context, url, cron string) (string, error) {
|
|
||||||
now := time.Now()
|
|
||||||
id := uuid.New().String()
|
|
||||||
return id, db.Exec(ctx, `
|
|
||||||
BEGIN;
|
|
||||||
INSERT INTO "feed.entries" (
|
|
||||||
id,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES ($1, $2, $3);
|
|
||||||
INSERT INTO "feed.versions" (
|
|
||||||
entries_id,
|
|
||||||
created_at,
|
|
||||||
url,
|
|
||||||
cron
|
|
||||||
) VALUES ($4, $5, $6, $7);
|
|
||||||
COMMIT;
|
|
||||||
`,
|
|
||||||
id, now, now,
|
|
||||||
id, now, url, cron,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Feeds) Update(ctx context.Context, id string, url, cron *string) error {
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Feeds) Delete(ctx context.Context, id string) error {
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Feeds) getEntry(ctx context.Context, id string) (Entry, error) {
|
|
||||||
return db.QueryOne[Entry](ctx, `
|
|
||||||
SELECT
|
|
||||||
id AS ID,
|
|
||||||
created_at AS Created,
|
|
||||||
updated_at AS Updated,
|
|
||||||
deleted_at AS Deleted
|
|
||||||
FROM
|
|
||||||
"feed.entries"
|
|
||||||
WHERE
|
|
||||||
id = ?
|
|
||||||
`, id)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"show-rss/src/db"
|
"show-rss/src/db"
|
||||||
"show-rss/src/feeds"
|
"show-rss/src/feeds"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -13,41 +12,15 @@ func TestFeeds(t *testing.T) {
|
|||||||
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer can()
|
defer can()
|
||||||
|
|
||||||
t.Run("same ctx", func(t *testing.T) {
|
|
||||||
ctx := db.Test(t, ctx)
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
|
||||||
if _, err := feeds.New(ctx); err != nil && ctx.Err() == nil {
|
|
||||||
t.Fatalf("failed %d: %v", i, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("new ctx", func(t *testing.T) {
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
|
||||||
if _, err := feeds.New(db.Test(t, ctx)); err != nil && ctx.Err() == nil {
|
|
||||||
t.Fatalf("failed %d: %v", i, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("crud", func(t *testing.T) {
|
t.Run("crud", func(t *testing.T) {
|
||||||
ctx := db.Test(t, ctx)
|
ctx := db.Test(t, ctx)
|
||||||
|
|
||||||
f, err := feeds.New(ctx)
|
id, err := feeds.Insert(ctx, "url", "cron", "pattern")
|
||||||
if err != nil && ctx.Err() == nil {
|
|
||||||
t.Fatalf("failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := f.Insert(ctx, "url", "cron")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("cannot insert:", err)
|
t.Fatal("cannot insert:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := f.Get(ctx, id)
|
got, err := feeds.Get(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("cannot get:", err)
|
t.Fatal("cannot get:", err)
|
||||||
}
|
}
|
||||||
@@ -75,6 +48,9 @@ func TestFeeds(t *testing.T) {
|
|||||||
if got.Version.Cron != "cron" {
|
if got.Version.Cron != "cron" {
|
||||||
t.Error("no version.cron")
|
t.Error("no version.cron")
|
||||||
}
|
}
|
||||||
|
if got.Version.Pattern != "pattern" {
|
||||||
|
t.Error("bad version.pattern")
|
||||||
|
}
|
||||||
|
|
||||||
if !got.Execution.Executed.IsZero() {
|
if !got.Execution.Executed.IsZero() {
|
||||||
t.Error("execution.executed")
|
t.Error("execution.executed")
|
||||||
@@ -82,5 +58,50 @@ func TestFeeds(t *testing.T) {
|
|||||||
if !got.Execution.Version.IsZero() {
|
if !got.Execution.Version.IsZero() {
|
||||||
t.Error("execution.version")
|
t.Error("execution.version")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := got.Executed(ctx); err != nil {
|
||||||
|
t.Fatal("cannot executed:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got2, err := feeds.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot get w executed:", err)
|
||||||
|
}
|
||||||
|
t.Logf("%+v", got2)
|
||||||
|
|
||||||
|
if got2.Execution.Executed.IsZero() {
|
||||||
|
t.Error("no execution.executed")
|
||||||
|
}
|
||||||
|
if got2.Execution.Version != got.Version.Created {
|
||||||
|
t.Errorf("bad execution.version: expected %v but got %v (difference of %v)", got.Version.Created, got2.Execution.Version, got2.Execution.Version.Sub(got.Execution.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
got2.Execution = got.Execution
|
||||||
|
if got != got2 {
|
||||||
|
t.Errorf("changes after execution: was \n\t%+v but now \n\t%+v", got, got2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := got.Executed(ctx); err != nil {
|
||||||
|
t.Fatal("cannot executed again:", err)
|
||||||
|
}
|
||||||
|
got3, err := feeds.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot get w executed again:", err)
|
||||||
|
} else if got2.Execution == got3.Execution {
|
||||||
|
t.Errorf("getting after second execution returned first execution")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
if err := feeds.ForEach(ctx, func(feed feeds.Feed) error {
|
||||||
|
n += 1
|
||||||
|
if feed != got3 {
|
||||||
|
t.Errorf("for each yielded difference than last get")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Errorf("for each didnt hit known get")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/feeds/http.go
Normal file
35
src/feeds/http.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package feeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (feed Feed) ShouldExecute() (bool, error) {
|
||||||
|
if feed.Entry.Deleted.IsZero() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule, err := cron.NewParser(
|
||||||
|
cron.SecondOptional |
|
||||||
|
cron.Minute |
|
||||||
|
cron.Hour |
|
||||||
|
cron.Dom |
|
||||||
|
cron.Month |
|
||||||
|
cron.Dow |
|
||||||
|
cron.Descriptor,
|
||||||
|
).Parse(feed.Version.Cron)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("illegal cron %q", feed.Version.Cron)
|
||||||
|
}
|
||||||
|
next := schedule.Next(feed.Execution.Executed)
|
||||||
|
return time.Now().Before(next), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (feed Feed) Fetch(ctx context.Context) (Items, error) {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
49
src/feeds/http_test.go
Normal file
49
src/feeds/http_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package feeds_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"show-rss/src/feeds"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeedFetch(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t.Logf("%s", r.URL.String())
|
||||||
|
}))
|
||||||
|
t.Cleanup(s.Close)
|
||||||
|
|
||||||
|
created := time.Now().Add(-4 * time.Second)
|
||||||
|
feed := feeds.Feed{
|
||||||
|
Entry: feeds.Entry{
|
||||||
|
ID: "id",
|
||||||
|
Created: created,
|
||||||
|
Updated: created,
|
||||||
|
Deleted: time.Time{},
|
||||||
|
},
|
||||||
|
Version: feeds.Version{
|
||||||
|
Created: created,
|
||||||
|
URL: s.URL,
|
||||||
|
Cron: "* * * * *",
|
||||||
|
Pattern: "matches",
|
||||||
|
},
|
||||||
|
Execution: feeds.Execution{
|
||||||
|
Executed: created.Add(-2 * time.Second),
|
||||||
|
Version: created,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := feed.Fetch(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed fetch: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) != 2 {
|
||||||
|
t.Fatalf("expected 2 items but got %+v", items)
|
||||||
|
}
|
||||||
|
t.Errorf("check items")
|
||||||
|
}
|
||||||
5
src/feeds/item.go
Normal file
5
src/feeds/item.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package feeds
|
||||||
|
|
||||||
|
type Items []Item
|
||||||
|
|
||||||
|
type Item struct{}
|
||||||
Reference in New Issue
Block a user