refactor: push commands in to cmd and ev to root as library

This commit is contained in:
Jon Lundy
2023-01-09 11:30:02 -07:00
parent 250395d6b3
commit 4fc9c78dae
58 changed files with 765 additions and 376 deletions

3
pkg/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Pkg Tools
This is a collection of modules that provide simple reusable functions.

View File

@@ -60,7 +60,7 @@ func parseInto(c string, s *set.BoundSet[int8]) *set.BoundSet[int8] {
// 24hour time. Any of the values may be -1 as an "any" match, so passing in
// a day of -1, the event occurs every day; passing in a second value of -1, the
// event will fire every second that the other parameters match.
func (c *cron) NewJob(expr string, task task) {
func (c *cron) NewCron(expr string, task func(context.Context, time.Time) error) {
sp := append(strings.Fields(expr), make([]string, 5)...)[:5]
job := job{
@@ -73,7 +73,7 @@ func (c *cron) NewJob(expr string, task task) {
}
c.jobs = append(c.jobs, job)
}
func (c *cron) Once(ctx context.Context, once task) {
func (c *cron) RunOnce(ctx context.Context, once func(context.Context, time.Time) error) {
c.state.Modify(ctx, func(ctx context.Context, state *state) error {
state.queue = append(state.queue, once)
return nil

40
pkg/env/env.go vendored Normal file
View File

@@ -0,0 +1,40 @@
package env
import (
"log"
"os"
"strings"
)
func Default(name, defaultValue string) string {
name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
log.Println("# ", name, "=", v)
return v
}
log.Println("# ", name, "=", defaultValue, "(default)")
return defaultValue
}
type secret string
func (s secret) String() string {
if s == "" {
return "(nil)"
}
return "***"
}
func (s secret) Secret() string {
return string(s)
}
func Secret(name, defaultValue string) secret {
name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
log.Println("# ", name, "=", secret(v))
return secret(v)
}
log.Println("# ", name, "=", secret(defaultValue), "(default)")
return secret(defaultValue)
}

View File

@@ -16,9 +16,9 @@ import (
"go.opentelemetry.io/otel/metric/instrument/syncint64"
"go.uber.org/multierr"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/cache"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker"
@@ -41,8 +41,8 @@ type diskStore struct {
m_disk_write syncint64.Counter
}
const AppendOnly = es.AppendOnly
const AllEvents = es.AllEvents
const AppendOnly = ev.AppendOnly
const AllEvents = ev.AllEvents
func Init(ctx context.Context) error {
ctx, span := lg.Span(ctx)
@@ -65,7 +65,7 @@ func Init(ctx context.Context) error {
d.m_disk_write, err = m.SyncInt64().Counter("disk_write")
errs = multierr.Append(errs, err)
es.Register(ctx, "file", d)
ev.Register(ctx, "file", d)
return errs
}
@@ -204,7 +204,7 @@ func (e *eventLog) Append(ctx context.Context, events event.Events, version uint
}
if version != AppendOnly && version != last {
err = fmt.Errorf("%w: current version wrong %d != %d", es.ErrWrongVersion, version, last)
err = fmt.Errorf("%w: current version wrong %d != %d", ev.ErrWrongVersion, version, last)
span.RecordError(err)
return err
}
@@ -411,7 +411,7 @@ func readStream(ctx context.Context, stream *wal.Log, index uint64) (event.Event
b, err = stream.Read(index)
if err != nil {
if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) {
err = fmt.Errorf("%w: empty", es.ErrNotFound)
err = fmt.Errorf("%w: empty", ev.ErrNotFound)
}
span.RecordError(err)
@@ -444,7 +444,7 @@ func readStreamN(ctx context.Context, stream *wal.Log, index ...uint64) (event.E
b, err = stream.Read(idx)
if err != nil {
if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) {
err = fmt.Errorf("%w: empty", es.ErrNotFound)
err = fmt.Errorf("%w: empty", ev.ErrNotFound)
}
span.RecordError(err)

View File

@@ -5,8 +5,8 @@ import (
"context"
"fmt"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker"
@@ -24,14 +24,14 @@ type memstore struct {
state *locker.Locked[state]
}
const AppendOnly = es.AppendOnly
const AllEvents = es.AllEvents
const AppendOnly = ev.AppendOnly
const AllEvents = ev.AllEvents
func Init(ctx context.Context) error {
ctx, span := lg.Span(ctx)
defer span.End()
return es.Register(ctx, "mem", &memstore{})
return ev.Register(ctx, "mem", &memstore{})
}
var _ driver.Driver = (*memstore)(nil)
@@ -84,7 +84,7 @@ func (m *eventLog) Append(ctx context.Context, events event.Events, version uint
last := uint64(len(*stream))
if version != AppendOnly && version != last {
return fmt.Errorf("%w: current version wrong %d != %d", es.ErrWrongVersion, version, last)
return fmt.Errorf("%w: current version wrong %d != %d", ev.ErrWrongVersion, version, last)
}
for i := range events {

View File

@@ -5,8 +5,8 @@ import (
"context"
"strings"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
)
@@ -19,7 +19,7 @@ type projector struct {
func New(_ context.Context, fns ...func(event.Event) []event.Event) *projector {
return &projector{fns: fns}
}
func (p *projector) Apply(e *es.EventStore) {
func (p *projector) Apply(e *ev.EventStore) {
up := e.Driver
for up != nil {
@@ -29,7 +29,7 @@ func (p *projector) Apply(e *es.EventStore) {
return
}
up = es.Unwrap(up)
up = ev.Unwrap(up)
}
p.up = e.Driver
@@ -112,7 +112,7 @@ func (w *wrapper) Append(ctx context.Context, events event.Events, version uint6
span.RecordError(err)
continue
}
_, err = l.Append(ctx, event.NewEvents(e), es.AppendOnly)
_, err = l.Append(ctx, event.NewEvents(e), ev.AppendOnly)
span.RecordError(err)
}
}()

View File

@@ -5,7 +5,7 @@ import (
"testing"
"github.com/matryer/is"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/driver/projecter"
"github.com/sour-is/ev/pkg/es/event"
@@ -112,10 +112,10 @@ func TestProjecter(t *testing.T) {
return mockEL, nil
}
es.Init(ctx)
es.Register(ctx, "mock", mock)
ev.Init(ctx)
ev.Register(ctx, "mock", mock)
es, err := es.Open(
es, err := ev.Open(
ctx,
"mock:",
projecter.New(ctx, projecter.DefaultProjection),

View File

@@ -4,8 +4,8 @@ import (
"context"
"errors"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
)
@@ -18,7 +18,7 @@ func New() *resolvelinks {
return &resolvelinks{}
}
func (r *resolvelinks) Apply(es *es.EventStore) {
func (r *resolvelinks) Apply(es *ev.EventStore) {
r.up = es.Driver
es.Driver = r
}
@@ -77,7 +77,7 @@ func (w *wrapper) Read(ctx context.Context, after int64, count int64) (event.Eve
}
ptr := ptrs[streamID]
lis, err := d.ReadN(ctx, ids...)
if err != nil && !errors.Is(err, es.ErrNotFound) {
if err != nil && !errors.Is(err, ev.ErrNotFound) {
return nil, err
}

View File

@@ -9,8 +9,8 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker"
@@ -32,9 +32,9 @@ func New(ctx context.Context) *streamer {
return &streamer{state: locker.New(&state{subscribers: map[string][]*subscription{}})}
}
var _ es.Option = (*streamer)(nil)
var _ ev.Option = (*streamer)(nil)
func (s *streamer) Apply(e *es.EventStore) {
func (s *streamer) Apply(e *ev.EventStore) {
s.up = e.Driver
e.Driver = s
}
@@ -72,7 +72,7 @@ func (s *streamer) Subscribe(ctx context.Context, streamID string, start int64)
sub := &subscription{topic: streamID, events: events}
sub.position = locker.New(&position{
idx: start,
size: es.AllEvents,
size: ev.AllEvents,
})
sub.unsub = s.delete(streamID, sub)
@@ -232,7 +232,7 @@ func (s *subscription) Recv(ctx context.Context) <-chan bool {
_, span := lg.Span(ctx)
defer span.End()
if position.size == es.AllEvents {
if position.size == ev.AllEvents {
return nil
}
if position.size == 0 {

View File

@@ -1,415 +0,0 @@
// package es implements an event store and drivers for extending its functionality.
package es
import (
"context"
"errors"
"fmt"
"strings"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
"go.uber.org/multierr"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker"
)
type config struct {
drivers map[string]driver.Driver
}
const AppendOnly = ^uint64(0)
const AllEvents = int64(AppendOnly >> 1)
var (
drivers = locker.New(&config{drivers: make(map[string]driver.Driver)})
)
func Init(ctx context.Context) error {
m := lg.Meter(ctx)
var err, errs error
Mes_open, err = m.SyncInt64().Counter("es_open")
errs = multierr.Append(errs, err)
Mes_read, err = m.SyncInt64().Counter("es_read")
errs = multierr.Append(errs, err)
Mes_load, err = m.SyncInt64().Counter("es_load")
errs = multierr.Append(errs, err)
Mes_save, err = m.SyncInt64().Counter("es_save")
errs = multierr.Append(errs, err)
Mes_append, err = m.SyncInt64().Counter("es_append")
errs = multierr.Append(errs, err)
return errs
}
func Register(ctx context.Context, name string, d driver.Driver) error {
ctx, span := lg.Span(ctx)
defer span.End()
return drivers.Modify(ctx, func(ctx context.Context, c *config) error {
if _, set := c.drivers[name]; set {
return fmt.Errorf("driver %s already set", name)
}
c.drivers[name] = d
return nil
})
}
type EventStore struct {
driver.Driver
}
var (
Mes_open syncint64.Counter
Mes_read syncint64.Counter
Mes_load syncint64.Counter
Mes_save syncint64.Counter
Mes_append syncint64.Counter
)
func Open(ctx context.Context, dsn string, options ...Option) (*EventStore, error) {
ctx, span := lg.Span(ctx)
defer span.End()
span.SetAttributes(attribute.String("dsn", dsn))
name, _, ok := strings.Cut(dsn, ":")
if !ok {
return nil, fmt.Errorf("%w: no scheme", ErrNoDriver)
}
var d driver.Driver
c, err := drivers.Copy(ctx)
if err != nil {
return nil, err
}
d, ok = c.drivers[name]
if !ok {
return nil, fmt.Errorf("%w: %s not registered", ErrNoDriver, name)
}
conn, err := d.Open(ctx, dsn)
es := &EventStore{Driver: conn}
es.Option(options...)
Mes_open.Add(ctx, 1)
return es, err
}
func (es *EventStore) Option(options ...Option) {
for _, o := range options {
o.Apply(es)
}
}
type Option interface {
Apply(*EventStore)
}
func (es *EventStore) Save(ctx context.Context, agg event.Aggregate) (uint64, error) {
ctx, span := lg.Span(ctx)
defer span.End()
events := agg.Events(true)
if len(events) == 0 {
return 0, nil
}
span.SetAttributes(
attribute.String("agg.type", event.TypeOf(agg)),
attribute.String("agg.streamID", agg.StreamID()),
attribute.Int64("agg.version", int64(agg.StreamVersion())),
)
l, err := es.EventLog(ctx, agg.StreamID())
if err != nil {
return 0, err
}
count, err := l.Append(ctx, events, agg.StreamVersion())
if err != nil {
return 0, err
}
Mes_save.Add(ctx, int64(count))
agg.Commit()
return count, err
}
func (es *EventStore) Load(ctx context.Context, agg event.Aggregate) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.SetAttributes(
attribute.String("agg.type", event.TypeOf(agg)),
attribute.String("agg.streamID", agg.StreamID()),
)
l, err := es.Driver.EventLog(ctx, agg.StreamID())
if err != nil {
return err
}
events, err := l.Read(ctx, 0, AllEvents)
if err != nil {
return err
}
Mes_load.Add(ctx, events.Count())
event.Append(agg, events...)
span.SetAttributes(
attribute.Int64("agg.version", int64(agg.StreamVersion())),
)
return nil
}
func (es *EventStore) Read(ctx context.Context, streamID string, after, count int64) (event.Events, error) {
ctx, span := lg.Span(ctx)
defer span.End()
span.SetAttributes(
attribute.String("streamID", streamID),
attribute.Int64("after", after),
attribute.Int64("count", count),
)
l, err := es.Driver.EventLog(ctx, streamID)
if err != nil {
return nil, err
}
events, err := l.Read(ctx, after, count)
Mes_read.Add(ctx, events.Count())
return events, err
}
func (es *EventStore) ReadN(ctx context.Context, streamID string, index ...uint64) (event.Events, error) {
ctx, span := lg.Span(ctx)
defer span.End()
lis := make([]int64, len(index))
for i, j := range index {
lis[i] = int64(j)
}
span.SetAttributes(
attribute.String("streamID", streamID),
attribute.Int64Slice("index", lis),
)
l, err := es.Driver.EventLog(ctx, streamID)
if err != nil {
return nil, err
}
events, err := l.ReadN(ctx, index...)
Mes_read.Add(ctx, events.Count())
return events, err
}
func (es *EventStore) Append(ctx context.Context, streamID string, events event.Events) (uint64, error) {
ctx, span := lg.Span(ctx)
defer span.End()
Mes_append.Add(ctx, 1)
span.SetAttributes(
attribute.String("ev.streamID", streamID),
)
l, err := es.Driver.EventLog(ctx, streamID)
if err != nil {
return 0, err
}
return l.Append(ctx, events, AppendOnly)
}
func (es *EventStore) FirstIndex(ctx context.Context, streamID string) (uint64, error) {
ctx, span := lg.Span(ctx)
defer span.End()
span.SetAttributes(
attribute.String("ev.streamID", streamID),
)
l, err := es.Driver.EventLog(ctx, streamID)
if err != nil {
return 0, err
}
return l.FirstIndex(ctx)
}
func (es *EventStore) LastIndex(ctx context.Context, streamID string) (uint64, error) {
ctx, span := lg.Span(ctx)
defer span.End()
span.SetAttributes(
attribute.String("ev.streamID", streamID),
)
l, err := es.Driver.EventLog(ctx, streamID)
if err != nil {
return 0, err
}
return l.LastIndex(ctx)
}
func (es *EventStore) EventStream() driver.EventStream {
if es == nil {
return nil
}
d := es.Driver
for d != nil {
if d, ok := d.(driver.EventStream); ok {
return d
}
d = Unwrap(d)
}
return nil
}
func (es *EventStore) Truncate(ctx context.Context, streamID string, index int64) error {
ctx, span := lg.Span(ctx)
defer span.End()
up, err := es.Driver.EventLog(ctx, streamID)
if err != nil {
return err
}
for up != nil {
if up, ok := up.(driver.EventLogWithTruncate); ok {
err = up.Truncate(ctx, index)
if err != nil {
return err
}
}
up = Unwrap(up)
}
return ErrNoDriver
}
func Unwrap[T any](t T) T {
if unwrap, ok := any(t).(interface{ Unwrap() T }); ok {
return unwrap.Unwrap()
} else {
var zero T
return zero
}
}
var ErrNoDriver = errors.New("no driver")
var ErrWrongVersion = errors.New("wrong version")
var ErrShouldExist = event.ErrShouldExist
var ErrShouldNotExist = event.ErrShouldNotExist
var ErrNotFound = errors.New("not found")
type PA[T any] interface {
event.Aggregate
*T
}
type PE[T any] interface {
event.Event
*T
}
// Create uses fn to create a new aggregate and store in db.
func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
agg = new(A)
agg.SetStreamID(streamID)
span.SetAttributes(
attribute.String("agg.streamID", streamID),
)
if err = es.Load(ctx, agg); err != nil {
return
}
if err = event.NotExists(agg); err != nil {
return
}
if err = fn(ctx, agg); err != nil {
return
}
var i uint64
if i, err = es.Save(ctx, agg); err != nil {
return
}
span.AddEvent(fmt.Sprint("wrote events = ", i))
return
}
// Update uses fn to update an exsisting aggregate and store in db.
func Update[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
agg = new(A)
agg.SetStreamID(streamID)
span.SetAttributes(
attribute.String("agg.streamID", streamID),
)
if err = es.Load(ctx, agg); err != nil {
return
}
if err = event.ShouldExist(agg); err != nil {
return
}
if err = fn(ctx, agg); err != nil {
return
}
if _, err = es.Save(ctx, agg); err != nil {
return
}
return
}
// Update uses fn to update an exsisting aggregate and store in db.
func Upsert[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
agg = new(A)
agg.SetStreamID(streamID)
span.SetAttributes(
attribute.String("agg.streamID", streamID),
)
if err = es.Load(ctx, agg); err != nil {
return
}
if err = fn(ctx, agg); err != nil {
return
}
if err = event.ShouldExist(agg); err != nil {
return
}
if _, err = es.Save(ctx, agg); err != nil {
return
}
return
}

View File

@@ -1,224 +0,0 @@
package es_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"
"time"
"github.com/matryer/is"
"go.uber.org/multierr"
"github.com/sour-is/ev/app/peerfinder"
"github.com/sour-is/ev/pkg/es"
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
"github.com/sour-is/ev/pkg/es/driver/projecter"
resolvelinks "github.com/sour-is/ev/pkg/es/driver/resolve-links"
"github.com/sour-is/ev/pkg/es/driver/streamer"
"github.com/sour-is/ev/pkg/es/event"
)
var (
_ event.Event = (*ValueSet)(nil)
_ event.Aggregate = (*Thing)(nil)
)
type Thing struct {
Name string
Value string
event.AggregateRoot
}
func (a *Thing) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case *ValueSet:
a.Value = e.Value
}
}
}
func (a *Thing) OnSetValue(value string) error {
event.Raise(a, &ValueSet{Value: value})
return nil
}
type ValueSet struct {
Value string
eventMeta event.Meta
}
func (e *ValueSet) EventMeta() event.Meta {
if e == nil {
return event.Meta{}
}
return e.eventMeta
}
func (e *ValueSet) SetEventMeta(eventMeta event.Meta) {
if e == nil {
return
}
e.eventMeta = eventMeta
}
func (e *ValueSet) MarshalBinary() ([]byte, error) {
return json.Marshal(e)
}
func (e *ValueSet) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, e)
}
func TestES(t *testing.T) {
is := is.New(t)
ctx := context.Background()
err := event.Register(ctx, &ValueSet{})
is.NoErr(err)
{
store, err := es.Open(ctx, "mem")
is.True(errors.Is(err, es.ErrNoDriver))
is.True(store.EventStream() == nil)
}
{
_, err := es.Open(ctx, "bogo:")
is.True(errors.Is(err, es.ErrNoDriver))
}
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
is.NoErr(err)
thing := &Thing{Name: "time"}
err = store.Load(ctx, thing)
is.NoErr(err)
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
err = thing.OnSetValue(time.Now().String())
is.NoErr(err)
thing.SetStreamID("thing-time")
i, err := store.Save(ctx, thing)
is.NoErr(err)
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
t.Log("Wrote: ", i)
i, err = store.Append(ctx, "thing-time", event.NewEvents(&ValueSet{Value: "xxx"}))
is.NoErr(err)
is.Equal(i, uint64(1))
events, err := store.Read(ctx, "thing-time", -1, -11)
is.NoErr(err)
for i, e := range events {
t.Logf("event %d %d - %v\n", i, e.EventMeta().Position, e)
}
first, err := store.FirstIndex(ctx, "thing-time")
is.NoErr(err)
is.Equal(first, uint64(1))
last, err := store.LastIndex(ctx, "thing-time")
is.NoErr(err)
is.Equal(last, uint64(2))
stream := store.EventStream()
is.True(stream != nil)
}
func TestESOperations(t *testing.T) {
is := is.New(t)
ctx := context.Background()
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
is.NoErr(err)
thing, err := es.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("foo")
})
is.NoErr(err)
is.Equal(thing.Version(), uint64(1))
is.Equal(thing.Value, "foo")
thing, err = es.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("bar")
})
is.NoErr(err)
is.Equal(thing.Version(), uint64(2))
is.Equal(thing.Value, "bar")
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("bin")
})
is.NoErr(err)
is.Equal(thing.Version(), uint64(1))
is.Equal(thing.Value, "bin")
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("baz")
})
is.NoErr(err)
is.Equal(thing.Version(), uint64(2))
is.Equal(thing.Value, "baz")
}
func TestUnwrap(t *testing.T) {
is := is.New(t)
err := errors.New("foo")
werr := fmt.Errorf("wrap: %w", err)
is.Equal(es.Unwrap(werr), err)
is.Equal(es.Unwrap("test"), "")
}
func TestUnwrapProjector(t *testing.T) {
is := is.New(t)
ctx, stop := context.WithCancel(context.Background())
defer stop()
es, err := es.Open(
ctx,
"mem:",
resolvelinks.New(),
streamer.New(ctx),
projecter.New(
ctx,
projecter.DefaultProjection,
peerfinder.Projector,
),
)
is.NoErr(err)
stream := es.EventStream()
is.True(stream != nil)
}
func TestMain(m *testing.M) {
ctx, stop := context.WithCancel(context.Background())
defer stop()
err := multierr.Combine(
es.Init(ctx),
event.Init(ctx),
memstore.Init(ctx),
)
if err != nil {
fmt.Println(err)
return
}
m.Run()
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"time"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql"
@@ -23,13 +24,17 @@ type contextKey struct {
var esKey = contextKey{"event-store"}
type EventStore struct {
*ev.EventStore
}
func (es *EventStore) IsResolver() {}
func (es *EventStore) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
ctx, span := lg.Span(ctx)
defer span.End()
lis, err := es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
if err != nil && !errors.Is(err, ErrNotFound) {
if err != nil && !errors.Is(err, ev.ErrNotFound) {
span.RecordError(err)
return nil, err
}

View File

@@ -52,12 +52,14 @@ outer:
rs := reflect.ValueOf(resolvers[i])
if field.IsNil() && rs.Type().Implements(field.Type()) {
// log.Print("found ", field.Type().Name())
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
field.Set(rs)
continue outer
}
}
// log.Print(fmt.Sprint("default ", field.Type().Name()))
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
field.Set(noop)
}

45
pkg/mux/httpmux.go Normal file
View File

@@ -0,0 +1,45 @@
package mux
import (
"net/http"
)
type mux struct {
*http.ServeMux
api *http.ServeMux
wellknown *http.ServeMux
}
func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
for _, fn := range fns {
// log.Printf("HTTP: %T", fn)
fn.RegisterHTTP(mux.ServeMux)
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
// log.Printf("APIv1: %T", fn)
fn.RegisterAPIv1(mux.api)
}
if fn, ok := fn.(interface{ RegisterWellKnown(*http.ServeMux) }); ok {
// log.Printf("APIv1: %T", fn)
fn.RegisterWellKnown(mux.wellknown)
}
}
}
func New() *mux {
mux := &mux{
api: http.NewServeMux(),
wellknown: http.NewServeMux(),
ServeMux: http.NewServeMux(),
}
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
mux.Handle("/.well-known/", http.StripPrefix("/.well-known/", mux.api))
return mux
}
type RegisterHTTP func(*http.ServeMux)
func (fn RegisterHTTP) RegisterHTTP(mux *http.ServeMux) {
fn(mux)
}

41
pkg/mux/httpmux_test.go Normal file
View File

@@ -0,0 +1,41 @@
package mux_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/matryer/is"
"github.com/sour-is/ev/pkg/mux"
)
type mockHTTP struct {
onServeHTTP func()
}
func (m *mockHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.onServeHTTP()
}
func (h *mockHTTP) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/", h)
}
func (h *mockHTTP) RegisterAPIv1(mux *http.ServeMux) {
mux.Handle("/ping", h)
}
func TestHttpMux(t *testing.T) {
is := is.New(t)
called := false
mux := mux.New()
mux.Add(&mockHTTP{func() { called = true }})
is.True(mux != nil)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/api/v1/ping", nil)
mux.ServeHTTP(w, r)
is.True(called)
}

169
pkg/service/service.go Normal file
View File

@@ -0,0 +1,169 @@
package service
import (
"context"
"log"
"net/http"
"runtime/debug"
"sort"
"strings"
"time"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/cron"
"go.opentelemetry.io/otel/attribute"
"go.uber.org/multierr"
"golang.org/x/sync/errgroup"
)
type crontab interface {
NewCron(expr string, task func(context.Context, time.Time) error)
RunOnce(ctx context.Context, once func(context.Context, time.Time) error)
}
type Harness struct {
crontab
Services []any
onStart []func(context.Context) error
onStop []func(context.Context) error
}
func (s *Harness) Setup(ctx context.Context, apps ...application) error {
ctx, span := lg.Span(ctx)
defer span.End()
// setup crontab
c := cron.New(cron.DefaultGranularity)
s.OnStart(c.Run)
s.crontab = c
var err error
for _, app := range apps {
err = multierr.Append(err, app(ctx, s))
}
span.RecordError(err)
return err
}
func (s *Harness) OnStart(fn func(context.Context) error) {
s.onStart = append(s.onStart, fn)
}
func (s *Harness) OnStop(fn func(context.Context) error) {
s.onStop = append(s.onStop, fn)
}
func (s *Harness) Add(svcs ...any) {
s.Services = append(s.Services, svcs...)
}
func (s *Harness) stop(ctx context.Context) error {
g, _ := errgroup.WithContext(ctx)
for i := range s.onStop {
fn := s.onStop[i]
g.Go(func() error {
if err := fn(ctx); err != nil && err != http.ErrServerClosed {
return err
}
return nil
})
}
return g.Wait()
}
func (s *Harness) Run(ctx context.Context, appName, version string) error {
{
ctx, span := lg.Span(ctx)
log.Println(appName, version)
span.SetAttributes(
attribute.String("app", appName),
attribute.String("version", version),
)
Mup, err := lg.Meter(ctx).SyncInt64().UpDownCounter("up")
if err != nil {
return err
}
Mup.Add(ctx, 1)
span.End()
}
g, _ := errgroup.WithContext(ctx)
g.Go(func() error {
<-ctx.Done()
// shutdown jobs
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.stop(ctx)
})
for i := range s.onStart {
fn := s.onStart[i]
g.Go(func() error { return fn(ctx) })
}
return g.Wait()
}
type application func(context.Context, *Harness) error // Len is the number of elements in the collection.
type appscore struct {
score int
application
}
type Apps []appscore
func (a *Apps) Apps() []application {
sort.Sort(a)
lis := make([]application, len(*a))
for i, app := range *a {
lis[i] = app.application
}
return lis
}
// Len is the number of elements in the collection.
func (a *Apps) Len() int {
if a == nil {
return 0
}
return len(*a)
}
// Less reports whether the element with index i
func (a *Apps) Less(i int, j int) bool {
if a == nil {
return false
}
return (*a)[i].score < (*a)[j].score
}
// Swap swaps the elements with indexes i and j.
func (a *Apps) Swap(i int, j int) {
if a == nil {
return
}
(*a)[i], (*a)[j] = (*a)[j], (*a)[i]
}
func (a *Apps) Register(score int, app application) (none struct{}) {
if a == nil {
return
}
*a = append(*a, appscore{score, app})
return
}
func AppName() (string, string) {
if info, ok := debug.ReadBuildInfo(); ok {
_, name, _ := strings.Cut(info.Main.Path, "/")
name = strings.Replace(name, "-", ".", -1)
name = strings.Replace(name, "/", "-", -1)
return name, info.Main.Version
}
return "sour.is-app", "(devel)"
}

35
pkg/slice/slice.go Normal file
View File

@@ -0,0 +1,35 @@
package slice
// FilterType returns a subset that matches the type.
func FilterType[T any](in ...any) []T {
lis := make([]T, 0, len(in))
for _, u := range in {
if t, ok := u.(T); ok {
lis = append(lis, t)
}
}
return lis
}
// Find returns the first of type found. or false if not found.
func Find[T any](in ...any) (T, bool) {
return First(FilterType[T](in...)...)
}
// First returns the first element in a slice.
func First[T any](in ...T) (T, bool) {
if len(in) == 0 {
var zero T
return zero, false
}
return in[0], true
}
// Map applys func to each element s and returns results as slice.
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}