Add base
This commit is contained in:
commit
085535931f
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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
|
105
ceen.go
Normal file
105
ceen.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package ceen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/kjuulh/ceen/codec"
|
||||||
|
"github.com/kjuulh/ceen/id"
|
||||||
|
"github.com/kjuulh/ceen/types"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ceen struct {
|
||||||
|
nc *nats.Conn
|
||||||
|
js nats.JetStreamContext
|
||||||
|
types *types.Registry
|
||||||
|
id id.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type ceenOption func(o *Ceen) error
|
||||||
|
|
||||||
|
func (f ceenOption) addOption(o *Ceen) error {
|
||||||
|
return f(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CeenOption interface {
|
||||||
|
addOption(o *Ceen) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func TypeRegistry(types *types.Registry) CeenOption {
|
||||||
|
return ceenOption(func(o *Ceen) error {
|
||||||
|
o.types = types
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ceen) EventStore(name string) (*EventStore, error) {
|
||||||
|
return &EventStore{name: name, c: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ceen) UnpackEvent(msg *nats.Msg) (*Event, error) {
|
||||||
|
eventType := msg.Header.Get(eventTypeHdr)
|
||||||
|
codecName := msg.Header.Get(eventCodecHdr)
|
||||||
|
var (
|
||||||
|
data any
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
codc, ok := codec.Codecs[codecName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %s", codec.ErrCodecNotRegistered, codecName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.types == nil {
|
||||||
|
var b []byte
|
||||||
|
err = codc.Unmarshal(msg.Data, &b)
|
||||||
|
data = b
|
||||||
|
} else {
|
||||||
|
var v any
|
||||||
|
v, err = c.types.Init(eventType)
|
||||||
|
if err == nil {
|
||||||
|
err = codc.Unmarshal(msg.Data, v)
|
||||||
|
data = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var seq uint64
|
||||||
|
if msg.Reply != "" {
|
||||||
|
md, err := msg.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unpack: failed to get metadata: %s", err)
|
||||||
|
}
|
||||||
|
seq = md.Sequence.Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Event{
|
||||||
|
ID: msg.Header.Get(nats.MsgIdHdr),
|
||||||
|
Type: msg.Header.Get(eventTypeHdr),
|
||||||
|
Data: data,
|
||||||
|
Subject: msg.Subject,
|
||||||
|
Sequence: seq,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(nc *nats.Conn, options ...CeenOption) (*Ceen, error) {
|
||||||
|
js, err := nc.JetStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Ceen{
|
||||||
|
nc: nc,
|
||||||
|
js: js,
|
||||||
|
id: id.NUID,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range options {
|
||||||
|
if err := o.addOption(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
47
codec/binary.go
Normal file
47
codec/binary.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Binary Codec = &binaryCodec{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type binaryCodec struct{}
|
||||||
|
|
||||||
|
func (*binaryCodec) Name() string {
|
||||||
|
return "binary"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*binaryCodec) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
// Check for native implementation.
|
||||||
|
if m, ok := v.(encoding.BinaryMarshaler); ok {
|
||||||
|
return m.MarshalBinary()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise assume byte slice.
|
||||||
|
b, ok := v.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("value not []byte")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*binaryCodec) Unmarshal(b []byte, v interface{}) error {
|
||||||
|
// Check for native implementation.
|
||||||
|
if u, ok := v.(encoding.BinaryUnmarshaler); ok {
|
||||||
|
return u.UnmarshalBinary(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise assume byte slice.
|
||||||
|
bp, ok := v.(*[]byte)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("value must be *[]byte")
|
||||||
|
}
|
||||||
|
|
||||||
|
*bp = append((*bp)[:0], b...)
|
||||||
|
return nil
|
||||||
|
}
|
20
codec/codec.go
Normal file
20
codec/codec.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCodecNotRegistered = errors.New("ceen: codec not registered")
|
||||||
|
|
||||||
|
Default = JSON
|
||||||
|
|
||||||
|
Codecs = map[string]Codec{
|
||||||
|
JSON.Name(): JSON,
|
||||||
|
Binary.Name(): Binary,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Codec interface {
|
||||||
|
Name() string
|
||||||
|
Marshal(any) ([]byte, error)
|
||||||
|
Unmarshal([]byte, any) error
|
||||||
|
}
|
24
codec/json.go
Normal file
24
codec/json.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
JSON Codec = &jsonCodec{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonCodec struct{}
|
||||||
|
|
||||||
|
func (*jsonCodec) Name() string {
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*jsonCodec) Marshal(v any) ([]byte, error) {
|
||||||
|
return json.Marshal(v)
|
||||||
|
|
||||||
|
}
|
||||||
|
func (*jsonCodec) Unmarshal(b []byte, v any) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(b, v)
|
||||||
|
}
|
11
event.go
Normal file
11
event.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package ceen
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
ID string
|
||||||
|
|
||||||
|
Type string
|
||||||
|
|
||||||
|
Data any
|
||||||
|
Subject string
|
||||||
|
Sequence uint64
|
||||||
|
}
|
227
event_store.go
Normal file
227
event_store.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package ceen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/kjuulh/ceen/codec"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventTypeHdr = "ceen-type"
|
||||||
|
eventCodecHdr = "ceen-codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventStore struct {
|
||||||
|
c *Ceen
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) Create(conf *nats.StreamConfig) error {
|
||||||
|
if conf == nil {
|
||||||
|
conf = &nats.StreamConfig{}
|
||||||
|
}
|
||||||
|
conf.Name = es.name
|
||||||
|
|
||||||
|
if len(conf.Subjects) == 0 {
|
||||||
|
conf.Subjects = []string{fmt.Sprintf("%s.>", es.name)}
|
||||||
|
}
|
||||||
|
_, err := es.c.js.AddStream(conf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) Append(ctx context.Context, subject string, events ...*Event) (uint64, error) {
|
||||||
|
var ack *nats.PubAck
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
popts := []nats.PubOpt{
|
||||||
|
nats.Context(ctx),
|
||||||
|
nats.ExpectStream(es.name),
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := es.wrapEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := es.packEvent(subject, e)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ack, err = es.c.js.PublishMsg(msg, popts...)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "wrong last sequence") {
|
||||||
|
return 0, errors.New("wrong last sequence")
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ack.Sequence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) wrapEvent(event *Event) (*Event, error) {
|
||||||
|
if event.Data == nil {
|
||||||
|
return nil, errors.New("event data is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if es.c.types == nil {
|
||||||
|
if event.Type == "" {
|
||||||
|
return nil, errors.New("event type is required")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t, err := es.c.types.Lookup(event.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Type == "" {
|
||||||
|
event.Type = t
|
||||||
|
} else if event.Type != t {
|
||||||
|
return nil, fmt.Errorf("wrong type for event data: %s", event.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := event.Data.(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if event.ID == "" {
|
||||||
|
event.ID = es.c.id.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) packEvent(subject string, event *Event) (*nats.Msg, error) {
|
||||||
|
var (
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
codecName string
|
||||||
|
)
|
||||||
|
|
||||||
|
if es.c.types == nil {
|
||||||
|
data, err = codec.Binary.Marshal(event.Data)
|
||||||
|
codecName = codec.Binary.Name()
|
||||||
|
} else {
|
||||||
|
data, err = es.c.types.Marshal(event.Data)
|
||||||
|
codecName = es.c.types.Codec().Name()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := nats.NewMsg(subject)
|
||||||
|
msg.Data = data
|
||||||
|
|
||||||
|
msg.Header.Set(nats.MsgIdHdr, event.ID)
|
||||||
|
msg.Header.Set(eventTypeHdr, event.Type)
|
||||||
|
msg.Header.Set(eventCodecHdr, codecName)
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type loadOpts struct {
|
||||||
|
afterSeq *uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type natsApiError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
ErrCode uint16 `json:"err_code"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type natsStoredMsg struct {
|
||||||
|
Sequence uint64 `json:"seq"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type natsGetMsgRequest struct {
|
||||||
|
LastBySubject string `json:"last_by_subj"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type natsGetMsgResponse struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Error *natsApiError `json:"error"`
|
||||||
|
Message *natsStoredMsg `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) Load(ctx context.Context, subject string) ([]*Event, uint64, error) {
|
||||||
|
lastMsg, err := es.lastMsgForSubject(ctx, subject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastMsg.Sequence == 0 {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sopts := []nats.SubOpt{
|
||||||
|
nats.OrderedConsumer(),
|
||||||
|
nats.DeliverAll(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := es.c.js.SubscribeSync(subject, sopts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer sub.Unsubscribe()
|
||||||
|
|
||||||
|
var events []*Event
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg, err := sub.NextMsgWithContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
event, err := es.c.UnpackEvent(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
events = append(events, event)
|
||||||
|
if event.Sequence == lastMsg.Sequence {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, lastMsg.Sequence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) lastMsgForSubject(ctx context.Context, subject string) (*natsStoredMsg, error) {
|
||||||
|
rsubject := fmt.Sprintf("$JS.API.STREAM.MSG.GET.%s", es.name)
|
||||||
|
|
||||||
|
data, _ := json.Marshal(&natsGetMsgRequest{
|
||||||
|
LastBySubject: subject,
|
||||||
|
})
|
||||||
|
|
||||||
|
msg, err := es.c.nc.RequestWithContext(ctx, rsubject, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rep natsGetMsgResponse
|
||||||
|
err = json.Unmarshal(msg.Data, &rep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rep.Error != nil {
|
||||||
|
if rep.Error.Code == 404 {
|
||||||
|
return &natsStoredMsg{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("%s (%d)", rep.Error.Description, rep.Error.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rep.Message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) Delete() error {
|
||||||
|
return es.c.js.DeleteStream(es.name)
|
||||||
|
}
|
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module github.com/kjuulh/ceen
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/nats-io/nats-server/v2 v2.8.4
|
||||||
|
github.com/nats-io/nats.go v1.16.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/klauspost/compress v1.14.4 // indirect
|
||||||
|
github.com/minio/highwayhash v1.0.2 // indirect
|
||||||
|
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a // indirect
|
||||||
|
github.com/nats-io/nkeys v0.3.0 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.1.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.7.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
47
go.sum
Normal file
47
go.sum
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=
|
||||||
|
github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||||
|
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||||
|
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a h1:lem6QCvxR0Y28gth9P+wV2K/zYUUAkJ+55U8cpS0p5I=
|
||||||
|
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.8.4 h1:0jQzze1T9mECg8YZEl8+WYUXb9JKluJfCBriPUtluB4=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.8.4/go.mod h1:8zZa+Al3WsESfmgSs98Fi06dRWLH5Bnq90m5bKD/eT4=
|
||||||
|
github.com/nats-io/nats.go v1.16.0 h1:zvLE7fGBQYW6MWaFaRdsgm9qT39PJDQoju+DS8KsO1g=
|
||||||
|
github.com/nats-io/nats.go v1.16.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||||
|
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||||
|
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||||
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
|
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
||||||
|
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
|
||||||
|
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
17
id/id.go
Normal file
17
id/id.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package id
|
||||||
|
|
||||||
|
import "github.com/nats-io/nuid"
|
||||||
|
|
||||||
|
var (
|
||||||
|
NUID ID = &nuidGen{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ID interface {
|
||||||
|
New() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type nuidGen struct{}
|
||||||
|
|
||||||
|
func (i *nuidGen) New() string {
|
||||||
|
return nuid.Next()
|
||||||
|
}
|
27
internal/testutil/testutil.go
Normal file
27
internal/testutil/testutil.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats-server/v2/server"
|
||||||
|
natsserver "github.com/nats-io/nats-server/v2/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewNatsServer(port int) *server.Server {
|
||||||
|
opts := natsserver.DefaultTestOptions
|
||||||
|
opts.Port = port
|
||||||
|
opts.JetStream = true
|
||||||
|
return natsserver.RunServer(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShutdownNatsServer(s *server.Server) {
|
||||||
|
var sd string
|
||||||
|
if config := s.JetStreamConfig(); config != nil {
|
||||||
|
sd = config.StoreDir
|
||||||
|
}
|
||||||
|
s.Shutdown()
|
||||||
|
if sd != "" {
|
||||||
|
os.RemoveAll(sd)
|
||||||
|
}
|
||||||
|
s.WaitForShutdown()
|
||||||
|
}
|
105
lib_test.go
Normal file
105
lib_test.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package ceen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/kjuulh/ceen/internal/testutil"
|
||||||
|
"github.com/kjuulh/ceen/types"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLib(t *testing.T) {
|
||||||
|
srv := testutil.NewNatsServer(-1)
|
||||||
|
defer testutil.ShutdownNatsServer(srv)
|
||||||
|
|
||||||
|
nc, _ := nats.Connect(srv.ClientURL())
|
||||||
|
|
||||||
|
c, err := New(nc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testItems, err := c.EventStore("test_items")
|
||||||
|
|
||||||
|
err = testItems.Create(&nats.StreamConfig{
|
||||||
|
Storage: nats.MemoryStorage,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
seq, err := testItems.Append(ctx, "test_items.1", &Event{Type: "test_item", Data: []byte("first-item")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint64(1), seq)
|
||||||
|
|
||||||
|
events, _, err := testItems.Load(ctx, "test_items.1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test_item", events[0].Type)
|
||||||
|
require.Equal(t, any([]byte("first-item")), events[0].Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestEventCreated struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibWithRegistry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
Run func(t *testing.T, es *EventStore, subject string)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "append-load-no-occ",
|
||||||
|
Run: func(t *testing.T, es *EventStore, subject string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
testEvent := TestEventCreated{ID: "some-event-id"}
|
||||||
|
seq, err := es.Append(ctx, subject, &Event{Data: &testEvent})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint64(1), seq)
|
||||||
|
|
||||||
|
events, lseq, err := es.Load(ctx, subject)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, seq, lseq)
|
||||||
|
|
||||||
|
require.True(t, events[0].ID != "")
|
||||||
|
require.Equal(t, "test-event-created", events[0].Type)
|
||||||
|
|
||||||
|
data, ok := events[0].Data.(*TestEventCreated)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, testEvent, *data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := testutil.NewNatsServer(-1)
|
||||||
|
defer testutil.ShutdownNatsServer(srv)
|
||||||
|
|
||||||
|
nc, err := nats.Connect(srv.ClientURL())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tr, err := types.NewRegistry(map[string]*types.Type{
|
||||||
|
"test-event-created": {
|
||||||
|
Init: func() any {
|
||||||
|
return &TestEventCreated{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c, err := New(nc, TypeRegistry(tr))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
|
es, err := c.EventStore("testevents")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = es.Delete()
|
||||||
|
err = es.Create(&nats.StreamConfig{Storage: nats.MemoryStorage})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
subject := fmt.Sprintf("testevents.%d", i)
|
||||||
|
|
||||||
|
test.Run(t, es, subject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
143
types/registry.go
Normal file
143
types/registry.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/kjuulh/ceen/codec"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTypeNotValid = errors.New("ceen: type not valid")
|
||||||
|
ErrTypeNotRegistered = errors.New("ceen: type not registered")
|
||||||
|
ErrNoTypeForStruct = errors.New("ceen: no type for struct")
|
||||||
|
ErrMarshal = errors.New("ceen: marshal error")
|
||||||
|
ErrUnmarshal = errors.New("ceen: unmarshal error")
|
||||||
|
|
||||||
|
nameRegex = regexp.MustCompile(`^[\w-]+(\.[\w-]+)*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type struct {
|
||||||
|
Init func() any
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
rtypes map[reflect.Type]string
|
||||||
|
types map[string]*Type
|
||||||
|
codec codec.Codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Lookup(v any) (string, error) {
|
||||||
|
ref := reflect.TypeOf(v)
|
||||||
|
t, ok := r.rtypes[ref]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("%w: %s", errors.New("no type for struct"), ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Init(eventType string) (any, error) {
|
||||||
|
t, ok := r.types[eventType]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrTypeNotRegistered, eventType)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := t.Init()
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) validate(name string, typeDec *Type) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%w: missing name", ErrTypeNotValid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateTypeName(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeDec.Init == nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrTypeNotValid, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := typeDec.Init()
|
||||||
|
if v == nil {
|
||||||
|
return fmt.Errorf("%w: %s: init func returns nil", ErrTypeNotValid, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := reflect.TypeOf(v)
|
||||||
|
|
||||||
|
if rt.Kind() != reflect.Ptr {
|
||||||
|
return fmt.Errorf("%w: %s: init func must return a pointer value", ErrTypeNotValid, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.Elem().Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("%w: %s", ErrTypeNotValid, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := r.codec.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %s: failed to marshal with codec: %s", ErrTypeNotValid, name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.codec.Unmarshal(b, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %s: failed to unmarshal with codec: %s", ErrTypeNotValid, name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTypeName(name string) error {
|
||||||
|
if !nameRegex.MatchString(name) {
|
||||||
|
return fmt.Errorf("%w: name %q has invalid characters", ErrTypeNotValid, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) addType(name string, typeDec *Type) {
|
||||||
|
r.types[name] = typeDec
|
||||||
|
|
||||||
|
v := typeDec.Init()
|
||||||
|
rt := reflect.TypeOf(v)
|
||||||
|
|
||||||
|
r.rtypes[rt] = name
|
||||||
|
r.rtypes[rt.Elem()] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Marshal(data any) ([]byte, error) {
|
||||||
|
_, err := r.Lookup(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := r.codec.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return b, fmt.Errorf("%T, marshal error: %w", data, err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Codec() codec.Codec {
|
||||||
|
return r.codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry(typeDecs map[string]*Type) (*Registry, error) {
|
||||||
|
r := &Registry{
|
||||||
|
rtypes: make(map[reflect.Type]string),
|
||||||
|
types: make(map[string]*Type),
|
||||||
|
codec: codec.Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, typeDec := range typeDecs {
|
||||||
|
err := r.validate(n, typeDec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.addType(n, typeDec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
5
validator.go
Normal file
5
validator.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package ceen
|
||||||
|
|
||||||
|
type validator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user