diff --git a/app/entry_register.go b/app/entry_register.go index 90c4e7e..125ae96 100644 --- a/app/entry_register.go +++ b/app/entry_register.go @@ -1,58 +1,11 @@ package app import ( - "errors" "owl-blogs/domain/model" - "reflect" ) -type EntryTypeRegistry struct { - types map[string]model.Entry -} +type EntryTypeRegistry = TypeRegistry[model.Entry] func NewEntryTypeRegistry() *EntryTypeRegistry { - return &EntryTypeRegistry{types: map[string]model.Entry{}} -} - -func (r *EntryTypeRegistry) entryType(entry model.Entry) string { - return reflect.TypeOf(entry).Elem().Name() -} - -func (r *EntryTypeRegistry) Register(entry model.Entry) error { - t := r.entryType(entry) - if _, ok := r.types[t]; ok { - return errors.New("entry type already registered") - } - r.types[t] = entry - return nil -} - -func (r *EntryTypeRegistry) Types() []model.Entry { - types := []model.Entry{} - for _, t := range r.types { - types = append(types, t) - } - return types -} - -func (r *EntryTypeRegistry) TypeName(entry model.Entry) (string, error) { - t := r.entryType(entry) - if _, ok := r.types[t]; !ok { - return "", errors.New("entry type not registered") - } - return t, nil -} - -func (r *EntryTypeRegistry) Type(name string) (model.Entry, error) { - if _, ok := r.types[name]; !ok { - return nil, errors.New("entry type not registered") - } - - val := reflect.ValueOf(r.types[name]) - if val.Kind() == reflect.Ptr { - val = reflect.Indirect(val) - } - newEntry := reflect.New(val.Type()).Interface().(model.Entry) - - return newEntry, nil + return NewTypeRegistry[model.Entry]() } diff --git a/app/generic_register.go b/app/generic_register.go new file mode 100644 index 0000000..8766c70 --- /dev/null +++ b/app/generic_register.go @@ -0,0 +1,57 @@ +package app + +import ( + "errors" + "reflect" +) + +type TypeRegistry[T any] struct { + types map[string]T +} + +func NewTypeRegistry[T any]() *TypeRegistry[T] { + return &TypeRegistry[T]{types: map[string]T{}} +} + +func (r *TypeRegistry[T]) entryType(entry T) string { + return reflect.TypeOf(entry).Elem().Name() +} + +func (r *TypeRegistry[T]) Register(entry T) error { + t := r.entryType(entry) + if _, ok := r.types[t]; ok { + return errors.New("entry type already registered") + } + r.types[t] = entry + return nil +} + +func (r *TypeRegistry[T]) Types() []T { + types := []T{} + for _, t := range r.types { + types = append(types, t) + } + return types +} + +func (r *TypeRegistry[T]) TypeName(entry T) (string, error) { + t := r.entryType(entry) + if _, ok := r.types[t]; !ok { + return "", errors.New("entry type not registered") + } + return t, nil +} + +func (r *TypeRegistry[T]) Type(name string) (T, error) { + if _, ok := r.types[name]; !ok { + return *new(T), errors.New("entry type not registered") + } + + val := reflect.ValueOf(r.types[name]) + if val.Kind() == reflect.Ptr { + val = reflect.Indirect(val) + } + newEntry := reflect.New(val.Type()).Interface().(T) + + return newEntry, nil +} diff --git a/app/interaction_register.go b/app/interaction_register.go new file mode 100644 index 0000000..c1c386b --- /dev/null +++ b/app/interaction_register.go @@ -0,0 +1,11 @@ +package app + +import ( + "owl-blogs/domain/model" +) + +type InteractionTypeRegistry = TypeRegistry[model.Interaction] + +func NewInteractionTypeRegistry() *InteractionTypeRegistry { + return NewTypeRegistry[model.Interaction]() +} diff --git a/app/repository/interfaces.go b/app/repository/interfaces.go index 7e671a4..262ed0e 100644 --- a/app/repository/interfaces.go +++ b/app/repository/interfaces.go @@ -36,3 +36,11 @@ type ConfigRepository interface { Get(name string, config interface{}) error Update(name string, siteConfig interface{}) error } + +type InteractionRepository interface { + Create(interaction model.Interaction) error + Update(interaction model.Interaction) error + Delete(interaction model.Interaction) error + FindById(id string) (model.Interaction, error) + FindAll(entryId string) ([]model.Interaction, error) +} diff --git a/app/webmention_service.go b/app/webmention_service.go new file mode 100644 index 0000000..c348204 --- /dev/null +++ b/app/webmention_service.go @@ -0,0 +1,18 @@ +package app + +import "owl-blogs/app/repository" + +type WebmentionService struct { + InteractionRepository repository.InteractionRepository + EntryRepository repository.EntryRepository +} + +func NewWebmentionService( + interactionRepository repository.InteractionRepository, + entryRepository repository.EntryRepository, +) *WebmentionService { + return &WebmentionService{ + InteractionRepository: interactionRepository, + EntryRepository: entryRepository, + } +} diff --git a/cmd/owl/main.go b/cmd/owl/main.go index ae74c6e..4a967a3 100644 --- a/cmd/owl/main.go +++ b/cmd/owl/main.go @@ -6,6 +6,7 @@ import ( "owl-blogs/app" entrytypes "owl-blogs/entry_types" "owl-blogs/infra" + "owl-blogs/interactions" "owl-blogs/web" "github.com/spf13/cobra" @@ -26,29 +27,41 @@ func Execute() { } func App(db infra.Database) *web.WebApp { - registry := app.NewEntryTypeRegistry() - registry.Register(&entrytypes.Image{}) - registry.Register(&entrytypes.Article{}) - registry.Register(&entrytypes.Page{}) - registry.Register(&entrytypes.Recipe{}) - registry.Register(&entrytypes.Note{}) - registry.Register(&entrytypes.Bookmark{}) - registry.Register(&entrytypes.Reply{}) + // Register Types + entryRegister := app.NewEntryTypeRegistry() + entryRegister.Register(&entrytypes.Image{}) + entryRegister.Register(&entrytypes.Article{}) + entryRegister.Register(&entrytypes.Page{}) + entryRegister.Register(&entrytypes.Recipe{}) + entryRegister.Register(&entrytypes.Note{}) + entryRegister.Register(&entrytypes.Bookmark{}) + entryRegister.Register(&entrytypes.Reply{}) - entryRepo := infra.NewEntryRepository(db, registry) - binRepo := infra.NewBinaryFileRepo(db) - authorRepo := infra.NewDefaultAuthorRepo(db) - siteConfigRepo := infra.NewConfigRepo(db) - - entryService := app.NewEntryService(entryRepo) - binaryService := app.NewBinaryFileService(binRepo) - authorService := app.NewAuthorService(authorRepo, siteConfigRepo) + interactionRegister := app.NewInteractionTypeRegistry() + interactionRegister.Register(&interactions.Webmention{}) configRegister := app.NewConfigRegister() + // Create Repositories + entryRepo := infra.NewEntryRepository(db, entryRegister) + binRepo := infra.NewBinaryFileRepo(db) + authorRepo := infra.NewDefaultAuthorRepo(db) + siteConfigRepo := infra.NewConfigRepo(db) + interactionRepo := infra.NewInteractionRepo(db, interactionRegister) + + // Create Services + entryService := app.NewEntryService(entryRepo) + binaryService := app.NewBinaryFileService(binRepo) + authorService := app.NewAuthorService(authorRepo, siteConfigRepo) + webmentionService := app.NewWebmentionService( + interactionRepo, entryRepo, + ) + + // Create WebApp return web.NewWebApp( - entryService, registry, binaryService, + entryService, entryRegister, binaryService, authorService, siteConfigRepo, configRegister, + webmentionService, ) } diff --git a/domain/model/interaction.go b/domain/model/interaction.go new file mode 100644 index 0000000..5f88215 --- /dev/null +++ b/domain/model/interaction.go @@ -0,0 +1,53 @@ +package model + +import "time" + +type InteractionContent string + +// Interaction is a generic interface for all interactions with entries +// These interactions can be: +// - Webmention, Pingback, Trackback +// - Likes, Comments on third party sites +// - Comments on the site itself +type Interaction interface { + ID() string + EntryID() string + Content() InteractionContent + CreatedAt() time.Time + MetaData() interface{} + + SetID(id string) + SetEntryID(entryID string) + SetCreatedAt(createdAt time.Time) + SetMetaData(metaData interface{}) +} + +type InteractionBase struct { + id string + entryID string + createdAt time.Time +} + +func (i *InteractionBase) ID() string { + return i.id +} + +func (i *InteractionBase) EntryID() string { + return i.entryID +} + +func (i *InteractionBase) CreatedAt() time.Time { + return i.createdAt +} + +func (i *InteractionBase) SetID(id string) { + i.id = id +} + +func (i *InteractionBase) SetEntryID(entryID string) { + i.entryID = entryID +} + +func (i *InteractionBase) SetCreatedAt(createdAt time.Time) { + i.createdAt = createdAt +} diff --git a/infra/entry_repository.go b/infra/entry_repository.go index 1316757..fc2580c 100644 --- a/infra/entry_repository.go +++ b/infra/entry_repository.go @@ -28,6 +28,26 @@ type DefaultEntryRepo struct { db *sqlx.DB } +func NewEntryRepository(db Database, register *app.EntryTypeRegistry) repository.EntryRepository { + sqlxdb := db.Get() + + // Create tables if not exists + sqlxdb.MustExec(` + CREATE TABLE IF NOT EXISTS entries ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + published_at DATETIME, + author_id TEXT NOT NULL, + meta_data TEXT NOT NULL + ); + `) + + return &DefaultEntryRepo{ + db: sqlxdb, + typeRegistry: register, + } +} + // Create implements repository.EntryRepository. func (r *DefaultEntryRepo) Create(entry model.Entry) error { t, err := r.typeRegistry.TypeName(entry) @@ -123,26 +143,6 @@ func (r *DefaultEntryRepo) Update(entry model.Entry) error { return err } -func NewEntryRepository(db Database, register *app.EntryTypeRegistry) repository.EntryRepository { - sqlxdb := db.Get() - - // Create tables if not exists - sqlxdb.MustExec(` - CREATE TABLE IF NOT EXISTS entries ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, - published_at DATETIME, - author_id TEXT NOT NULL, - meta_data TEXT NOT NULL - ); - `) - - return &DefaultEntryRepo{ - db: sqlxdb, - typeRegistry: register, - } -} - func (r *DefaultEntryRepo) sqlEntryToEntry(entry sqlEntry) (model.Entry, error) { e, err := r.typeRegistry.Type(entry.Type) if err != nil { diff --git a/infra/interaction_repository.go b/infra/interaction_repository.go new file mode 100644 index 0000000..68f4144 --- /dev/null +++ b/infra/interaction_repository.go @@ -0,0 +1,67 @@ +package infra + +import ( + "owl-blogs/app" + "owl-blogs/app/repository" + "owl-blogs/domain/model" + + "github.com/jmoiron/sqlx" +) + +type sqlInteraction struct { + Id string `db:"id"` + Type string `db:"type"` + EntryId string `db:"entry_id"` + CreatedAt string `db:"created_at"` + MetaData string `db:"meta_data"` +} + +type DefaultInteractionRepo struct { + typeRegistry *app.InteractionTypeRegistry + db *sqlx.DB +} + +func NewInteractionRepo(db Database, register *app.InteractionTypeRegistry) repository.InteractionRepository { + sqlxdb := db.Get() + + // Create tables if not exists + sqlxdb.MustExec(` + CREATE TABLE IF NOT EXISTS interactions ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + entry_id TEXT NOT NULL, + created_at DATETIME NOT NULL, + meta_data TEXT NOT NULL + ); + `) + + return &DefaultInteractionRepo{ + db: sqlxdb, + typeRegistry: register, + } +} + +// Create implements repository.InteractionRepository. +func (*DefaultInteractionRepo) Create(interaction model.Interaction) error { + panic("unimplemented") +} + +// Delete implements repository.InteractionRepository. +func (*DefaultInteractionRepo) Delete(interaction model.Interaction) error { + panic("unimplemented") +} + +// FindAll implements repository.InteractionRepository. +func (*DefaultInteractionRepo) FindAll(entryId string) ([]model.Interaction, error) { + panic("unimplemented") +} + +// FindById implements repository.InteractionRepository. +func (*DefaultInteractionRepo) FindById(id string) (model.Interaction, error) { + panic("unimplemented") +} + +// Update implements repository.InteractionRepository. +func (*DefaultInteractionRepo) Update(interaction model.Interaction) error { + panic("unimplemented") +} diff --git a/interactions/webmention.go b/interactions/webmention.go new file mode 100644 index 0000000..24816dc --- /dev/null +++ b/interactions/webmention.go @@ -0,0 +1,25 @@ +package interactions + +import "owl-blogs/domain/model" + +type Webmention struct { + model.InteractionBase + meta WebmentionInteractionMetaData +} + +type WebmentionInteractionMetaData struct { + Source string + Target string +} + +func (i *Webmention) Content() model.InteractionContent { + return model.InteractionContent(i.meta.Source) +} + +func (i *Webmention) MetaData() interface{} { + return &i.meta +} + +func (i *Webmention) SetMetaData(metaData interface{}) { + i.meta = *metaData.(*WebmentionInteractionMetaData) +} diff --git a/web/app.go b/web/app.go index debcfb5..5de6b92 100644 --- a/web/app.go +++ b/web/app.go @@ -34,6 +34,7 @@ func NewWebApp( authorService *app.AuthorService, configRepo repository.ConfigRepository, configRegister *app.ConfigRegister, + webmentionService *app.WebmentionService, ) *WebApp { app := fiber.New() app.Use(middleware.NewUserMiddleware(authorService).Handle)