chore: move apps to go.sour.is/tools

This commit is contained in:
xuu
2023-09-29 10:07:24 -06:00
parent 813c2e898d
commit ee45a0fd49
98 changed files with 35 additions and 12955 deletions

137
event/aggregate.go Normal file
View File

@@ -0,0 +1,137 @@
package event
import (
"errors"
"fmt"
"sync"
)
// Aggregate implements functionality for working with event store streams as an aggregate.
// When creating a new Aggregate the struct should have an ApplyEvent method and embed the AggregateRoot.
type Aggregate interface {
// ApplyEvent applies the event to the aggrigate state
ApplyEvent(...Event)
AggregateRoot
}
func Start(a Aggregate, i uint64) {
a.start(i)
}
// Raise adds new uncommitted events
func Raise(a Aggregate, lis ...Event) {
lis = NewEvents(lis...)
SetStreamID(a.StreamID(), lis...)
a.raise(lis...)
a.ApplyEvent(lis...)
}
// Append adds new committed events
func Append(a Aggregate, lis ...Event) {
a.append(lis...)
a.ApplyEvent(lis...)
}
// NotExists returns error if there are no events present.
func NotExists(a Aggregate) error {
if a.Version() != 0 {
return fmt.Errorf("%w, got version == %d", ErrShouldNotExist, a.Version())
}
return nil
}
// ShouldExists returns error if there are no events present.
func ShouldExist(a Aggregate) error {
if a.Version() == 0 {
return fmt.Errorf("%w, got version == %d", ErrShouldExist, a.Version())
}
return nil
}
type AggregateRoot interface {
// Events returns the aggregate events
// pass true for only uncommitted events
Events(bool) Events
// StreamID returns aggregate stream ID
StreamID() string
// SetStreamID sets aggregate stream ID
SetStreamID(streamID string)
// StreamVersion returns last commit events
StreamVersion() uint64
// Version returns the current aggrigate version. (committed + uncommitted)
Version() uint64
start(uint64)
raise(lis ...Event)
append(lis ...Event)
Commit()
}
var _ AggregateRoot = &IsAggregate{}
type IsAggregate struct {
events Events
streamID string
firstIndex uint64
lastIndex uint64
mu sync.RWMutex
}
func (a *IsAggregate) Commit() { a.lastIndex = uint64(len(a.events)) }
func (a *IsAggregate) StreamID() string { return a.streamID }
func (a *IsAggregate) SetStreamID(streamID string) { a.streamID = streamID }
func (a *IsAggregate) StreamVersion() uint64 { return a.lastIndex }
func (a *IsAggregate) Version() uint64 { return a.firstIndex + uint64(len(a.events)) }
func (a *IsAggregate) Events(new bool) Events {
a.mu.RLock()
defer a.mu.RUnlock()
events := a.events
if new {
events = events[a.lastIndex-a.firstIndex:]
}
lis := make(Events, len(events))
copy(lis, events)
return lis
}
func (a *IsAggregate) start(i uint64) {
a.firstIndex = i
a.lastIndex = i
}
//lint:ignore U1000 is called by embeded interface
func (a *IsAggregate) raise(lis ...Event) { //nolint
a.mu.Lock()
defer a.mu.Unlock()
a.posStartAt(lis...)
a.events = append(a.events, lis...)
}
//lint:ignore U1000 is called by embeded interface
func (a *IsAggregate) append(lis ...Event) {
a.mu.Lock()
defer a.mu.Unlock()
a.posStartAt(lis...)
a.events = append(a.events, lis...)
a.lastIndex += uint64(len(lis))
}
func (a *IsAggregate) posStartAt(lis ...Event) {
for i, e := range lis {
m := e.EventMeta()
m.Position = a.lastIndex + uint64(i) + 1
e.SetEventMeta(m)
}
}
var ErrShouldNotExist = errors.New("should not exist")
var ErrShouldExist = errors.New("should exist")

43
event/aggregate_test.go Normal file
View File

@@ -0,0 +1,43 @@
package event_test
import (
"testing"
"go.sour.is/ev/event"
)
type Agg struct {
Value string
event.IsAggregate
}
var _ event.Aggregate = (*Agg)(nil)
func (a *Agg) streamID() string {
return "value-" + a.Value
}
// ApplyEvent applies the event to the aggrigate state
func (a *Agg) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case *ValueApplied:
a.Value = e.Value
a.SetStreamID(a.streamID())
}
}
}
type ValueApplied struct {
Value string
event.IsEvent
}
var _ event.Event = (*ValueApplied)(nil)
func TestAggregate(t *testing.T) {
agg := &Agg{}
event.Append(agg, &ValueApplied{Value: "one"})
}

266
event/events.go Normal file
View File

@@ -0,0 +1,266 @@
// package event implements functionality for working with an eventstore.
package event
import (
"context"
"crypto/rand"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
ulid "github.com/oklog/ulid/v2"
)
var pool = sync.Pool{
New: func() interface{} { return ulid.Monotonic(rand.Reader, 0) },
}
func getULID() ulid.ULID {
var entropy io.Reader = rand.Reader
if e, ok := pool.Get().(io.Reader); ok {
entropy = e
defer pool.Put(e)
}
return ulid.MustNew(ulid.Now(), entropy)
}
// Event implements functionality of an individual event used with the event store. It should implement the getter/setter for EventMeta and BinaryMarshaler/BinaryUnmarshaler.
type Event interface {
EventMeta() Meta
SetEventMeta(Meta)
}
// Events is a list of events
type Events []Event
func NewEvents(lis ...Event) Events {
for i, e := range lis {
meta := e.EventMeta()
meta.Position = uint64(i)
if meta.ActualPosition == 0 {
meta.ActualPosition = uint64(i)
}
meta.EventID = getULID()
e.SetEventMeta(meta)
}
return lis
}
func (lis Events) StreamID() string {
if len(lis) == 0 {
return ""
}
return lis.First().EventMeta().StreamID
}
func (lis Events) SetStreamID(streamID string) {
SetStreamID(streamID, lis...)
}
func (lis Events) Count() int64 {
return int64(len(lis))
}
func (lis Events) First() Event {
if len(lis) == 0 {
return NilEvent
}
return lis[0]
}
func (lis Events) Rest() Events {
if len(lis) == 0 {
return nil
}
return lis[1:]
}
func (lis Events) Last() Event {
if len(lis) == 0 {
return NilEvent
}
return lis[len(lis)-1]
}
func TypeOf(e any) string {
if ie, ok := e.(interface{ UnwrapEvent() Event }); ok {
e = ie.UnwrapEvent()
}
if e, ok := e.(interface{ EventType() string }); ok {
return e.EventType()
}
// Default to printed representation for unnamed types
return strings.Trim(fmt.Sprintf("%T", e), "*")
}
type streamID string
func (s streamID) StreamID() string {
return string(s)
}
func StreamID(e Event) streamID {
return streamID(e.EventMeta().StreamID)
}
func SetStreamID(id string, lis ...Event) {
for _, e := range lis {
meta := e.EventMeta()
meta.StreamID = id
if meta.ActualStreamID == "" {
meta.ActualStreamID = id
}
e.SetEventMeta(meta)
}
}
func EventID(e Event) ulid.ULID {
return e.EventMeta().EventID
}
func SetEventID(e Event, id ulid.ULID) {
meta := e.EventMeta()
meta.EventID = id
e.SetEventMeta(meta)
}
func SetPosition(e Event, i uint64) {
meta := e.EventMeta()
meta.Position = i
meta.ActualPosition = i
e.SetEventMeta(meta)
}
type Meta struct {
EventID ulid.ULID
StreamID string
Position uint64
ActualStreamID string
ActualPosition uint64
}
func (m Meta) Created() time.Time {
return ulid.Time(m.EventID.Time())
}
func (m Meta) GetEventID() string { return m.EventID.String() }
func Init(ctx context.Context) error {
if err := Register(ctx, NilEvent, &EventPtr{}); err != nil {
return err
}
if err := RegisterName(ctx, "event.eventPtr", &EventPtr{}); err != nil {
return err
}
return nil
}
type nilEvent struct {
IsEvent
}
var NilEvent = &nilEvent{}
func (e *nilEvent) MarshalBinary() ([]byte, error) {
return nil, nil
}
func (e *nilEvent) UnmarshalBinary(b []byte) error {
return nil
}
type EventPtr struct {
StreamID string `json:"stream_id"`
Pos uint64 `json:"pos"`
IsEvent
}
var _ Event = (*EventPtr)(nil)
func NewPtr(streamID string, pos uint64) *EventPtr {
return &EventPtr{StreamID: streamID, Pos: pos}
}
// MarshalBinary implements Event
func (e *EventPtr) MarshalBinary() (data []byte, err error) {
return []byte(fmt.Sprintf("%s@%d", e.StreamID, e.Pos)), nil
}
// UnmarshalBinary implements Event
func (e *EventPtr) UnmarshalBinary(data []byte) error {
s := string(data)
idx := strings.LastIndex(s, "@")
if idx == -1 {
return fmt.Errorf("missing @ in: %s", s)
}
e.StreamID = s[:idx]
var err error
e.Pos, err = strconv.ParseUint(s[idx+1:], 10, 64)
return err
}
func (e *EventPtr) Values() any {
return struct {
StreamID string `json:"stream_id"`
Pos uint64 `json:"pos"`
}{
e.StreamID,
e.Pos,
}
}
type FeedTruncated struct {
IsEvent
}
func (e *FeedTruncated) Values() any {
return struct {
}{}
}
type property[T any] struct {
v T
}
type IsEvent = property[Meta]
func (p *property[T]) EventMeta() T {
if p == nil {
var t T
return t
}
return p.v
}
func (p *property[T]) SetEventMeta(x T) {
if p != nil {
p.v = x
}
}
func AsEvent[T any](e T) Event {
return &asEvent[T]{payload: e}
}
type asEvent[T any] struct {
payload T
IsEvent
}
func (e asEvent[T]) Payload() T {
return e.payload
}
type AGG interface{ ApplyEvent(...Event) }
func AsAggregate[T AGG](e T) Aggregate {
return &asAggregate[T]{payload: e}
}
type asAggregate[T AGG] struct {
payload T
IsAggregate
}
func (e *asAggregate[T]) Payload() T {
return e.payload
}
func (e *asAggregate[T]) ApplyEvent(lis ...Event) {
e.payload.ApplyEvent(lis...)
}

82
event/events_test.go Normal file
View File

@@ -0,0 +1,82 @@
package event_test
import (
"bytes"
"context"
"encoding/json"
"testing"
"github.com/matryer/is"
"go.sour.is/ev/event"
)
type DummyEvent struct {
Value string
event.IsEvent
}
func (e *DummyEvent) MarshalBinary() ([]byte, error) {
return json.Marshal(e)
}
func (e *DummyEvent) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, e)
}
func TestEventEncode(t *testing.T) {
is := is.New(t)
ctx := context.Background()
err := event.Register(ctx, &DummyEvent{})
is.NoErr(err)
var lis event.Events = event.NewEvents(
&DummyEvent{Value: "testA"},
&DummyEvent{Value: "testB"},
&DummyEvent{Value: "testC"},
)
lis.SetStreamID("test")
blis, err := event.EncodeEvents(lis...)
is.NoErr(err)
for _, b := range blis {
sp := bytes.SplitN(b, []byte{'\t'}, 4)
is.Equal(len(sp), 4)
is.Equal(string(sp[1]), "test")
is.Equal(string(sp[2]), "event_test.DummyEvent")
}
chk, err := event.DecodeEvents(ctx, blis...)
is.NoErr(err)
for i := range chk {
is.Equal(lis[i], chk[i])
}
}
type exampleAgg struct{ value string }
func (a *exampleAgg) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case interface{ Payload() exampleEvSetValue }:
a.value = e.Payload().value
}
}
}
type exampleEvSetValue struct{ value string }
func TestApplyEventGeneric(t *testing.T) {
payload := &exampleAgg{}
var agg = event.AsAggregate(payload)
agg.ApplyEvent(event.NewEvents(
event.AsEvent(exampleEvSetValue{"hello"}),
)...)
is := is.New(t)
is.Equal(payload.value, "hello")
}

320
event/reflect.go Normal file
View File

@@ -0,0 +1,320 @@
package event
import (
"bytes"
"context"
"encoding"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/locker"
)
type config struct {
eventTypes map[string]reflect.Type
}
var (
eventTypes = locker.New(&config{eventTypes: make(map[string]reflect.Type)})
)
type UnknownEvent struct {
eventType string
values map[string]json.RawMessage
IsEvent
}
var _ Event = (*UnknownEvent)(nil)
func NewUnknownEventFromValues(eventType string, meta Meta, values url.Values) *UnknownEvent {
jsonValues := make(map[string]json.RawMessage, len(values))
for k, v := range values {
switch len(v) {
case 0:
jsonValues[k] = []byte("null")
case 1:
jsonValues[k] = embedJSON(v[0])
default:
parts := make([][]byte, len(v))
for i := range v {
parts[i] = embedJSON(v[i])
}
jsonValues[k] = append([]byte("["), bytes.Join(parts, []byte(","))...)
jsonValues[k] = append(jsonValues[k], ']')
}
}
e := &UnknownEvent{eventType: eventType, values: jsonValues}
e.SetEventMeta(meta)
return e
}
func NewUnknownEventFromRaw(eventType string, meta Meta, values map[string]json.RawMessage) *UnknownEvent {
e := &UnknownEvent{eventType: eventType, values: values}
e.SetEventMeta(meta)
return e
}
func (u UnknownEvent) EventType() string { return u.eventType }
func (u *UnknownEvent) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, &u.values)
}
func (u *UnknownEvent) MarshalBinary() ([]byte, error) {
return json.Marshal(u.values)
}
// Register a type container for Unmarshalling values into. The type must implement Event and not be a nil value.
func Register(ctx context.Context, lis ...Event) error {
ctx, span := lg.Span(ctx)
defer span.End()
for _, e := range lis {
if err := ctx.Err(); err != nil {
span.RecordError(err)
return err
}
name := TypeOf(e)
err := RegisterName(ctx, name, e)
if err != nil {
return err
}
}
return nil
}
func RegisterName(ctx context.Context, name string, e Event) error {
ctx, span := lg.Span(ctx)
defer span.End()
if e == nil {
err := fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e)
span.RecordError(err)
return err
}
value := reflect.ValueOf(e)
if value.IsNil() {
err := fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e)
span.RecordError(err)
return err
}
value = reflect.Indirect(value)
typ := value.Type()
span.AddEvent("register: " + name)
if err := eventTypes.Use(ctx, func(ctx context.Context, c *config) error {
_, span := lg.Span(ctx)
defer span.End()
c.eventTypes[name] = typ
return nil
}); err != nil {
span.RecordError(err)
return err
}
return nil
}
func GetContainer(ctx context.Context, s string) Event {
ctx, span := lg.Span(ctx)
defer span.End()
var e Event
eventTypes.Use(ctx, func(ctx context.Context, c *config) error {
_, span := lg.Span(ctx)
defer span.End()
typ, ok := c.eventTypes[s]
if !ok {
err := fmt.Errorf("not defined: %s", s)
span.RecordError(err)
return err
}
newType := reflect.New(typ)
newInterface := newType.Interface()
if iface, ok := newInterface.(Event); ok {
e = iface
return nil
}
err := fmt.Errorf("failed")
span.RecordError(err)
return err
})
if e == nil {
e = &UnknownEvent{eventType: s}
}
return e
}
func MarshalBinary(e Event) ([]byte, error) {
var err error
b := &bytes.Buffer{}
m := e.EventMeta()
if _, err = b.WriteString(m.EventID.String()); err != nil {
return nil, err
}
b.WriteRune('\t')
if _, err = b.WriteString(m.StreamID); err != nil {
return nil, err
}
b.WriteRune('\t')
if _, err = b.WriteString(TypeOf(e)); err != nil {
return nil, err
}
b.WriteRune('\t')
switch e := e.(type) {
case encoding.BinaryMarshaler:
var txt []byte
if txt, err = e.MarshalBinary(); err != nil {
return nil, err
}
_, err = b.Write(txt)
case encoding.TextMarshaler:
var txt []byte
if txt, err = e.MarshalText(); err != nil {
return nil, err
}
_, err = b.Write(txt)
default:
err = json.NewEncoder(b).Encode(e)
}
return b.Bytes(), err
}
func UnmarshalBinary(ctx context.Context, txt []byte, pos uint64) (e Event, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
sp := bytes.SplitN(txt, []byte{'\t'}, 4)
if len(sp) != 4 {
err = fmt.Errorf("invalid format. expected=4, got=%d", len(sp))
span.RecordError(err)
return nil, err
}
m := Meta{}
if err = m.EventID.UnmarshalText(sp[0]); err != nil {
span.RecordError(err)
return nil, err
}
m.StreamID = string(sp[1])
m.Position = pos
m.ActualStreamID = string(sp[1])
m.ActualPosition = pos
eventType := string(sp[2])
e = GetContainer(ctx, eventType)
span.AddEvent(fmt.Sprintf("%s == %T", eventType, e))
switch e := e.(type) {
case encoding.BinaryUnmarshaler:
if err = e.UnmarshalBinary(sp[3]); err != nil {
span.RecordError(err)
return nil, err
}
case encoding.TextUnmarshaler:
if err = e.UnmarshalText(sp[3]); err != nil {
span.RecordError(err)
return nil, err
}
default:
if err = json.Unmarshal(sp[3], e); err != nil {
span.RecordError(err)
return nil, err
}
}
e.SetEventMeta(m)
return e, nil
}
// DecodeEvents unmarshals the byte list into Events.
func DecodeEvents(ctx context.Context, lis ...[]byte) (Events, error) {
elis := make([]Event, len(lis))
var err error
for i, txt := range lis {
elis[i], err = UnmarshalBinary(ctx, txt, uint64(i))
if err != nil {
return nil, err
}
}
return elis, nil
}
func EncodeEvents(events ...Event) (lis [][]byte, err error) {
lis = make([][]byte, len(events))
for i, txt := range events {
lis[i], err = MarshalBinary(txt)
if err != nil {
return nil, err
}
}
return lis, nil
}
func embedJSON(s string) json.RawMessage {
if len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}' {
return []byte(s)
}
if len(s) > 1 && s[0] == '[' && s[len(s)-1] == ']' {
return []byte(s)
}
return []byte(fmt.Sprintf(`"%s"`, strings.Replace(s, `"`, `\"`, -1)))
}
func Values(e Event) map[string]any {
var a any = e
if e, ok := e.(interface{ Values() any }); ok {
a = e.Values()
}
m := make(map[string]any)
v := reflect.Indirect(reflect.ValueOf(a))
for _, idx := range reflect.VisibleFields(v.Type()) {
if !idx.IsExported() {
continue
}
omitempty := false
field := v.FieldByIndex(idx.Index)
name := idx.Name
if n, ok := idx.Tag.Lookup("json"); ok {
var (
opt string
found bool
)
name, opt, found = strings.Cut(n, ",")
if name == "-" {
continue
}
if found {
if strings.Contains(opt, "omitempty") {
omitempty = true
}
}
}
if omitempty && field.IsZero() {
continue
}
m[name] = field.Interface()
}
return m
}