Files
2026-09-15 21:27:46 +05:00

371 lines
7.8 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/ThreeDotsLabs/watermill"
watermillHTTP "github.com/ThreeDotsLabs/watermill-http/v2/pkg/http"
"github.com/ThreeDotsLabs/watermill/message"
)
var generatedTags = []string{"watermill", "golang", "pubsub", "unicorn", "HelloWorld", "example", "ThreeDotsLabs"}
type Router struct {
Subscriber message.Subscriber
Publisher Publisher
PostsStorage PostsStorage
FeedsStorage FeedsStorage
Logger watermill.LoggerAdapter
}
func (router Router) Mux() *chi.Mux {
r := chi.NewRouter()
root := http.Dir("./public")
FileServer(r, "/", root)
sseRouter, err := watermillHTTP.NewSSERouter(
watermillHTTP.SSERouterConfig{
UpstreamSubscriber: router.Subscriber,
ErrorHandler: watermillHTTP.DefaultErrorHandler,
},
router.Logger,
)
if err != nil {
panic(err)
}
postStream := postStreamAdapter{storage: router.PostsStorage, logger: router.Logger}
feedStream := feedStreamAdapter{storage: router.FeedsStorage, logger: router.Logger}
allFeedsStream := allFeedsStreamAdapter{storage: router.FeedsStorage, logger: router.Logger}
postHandler := sseRouter.AddHandler(PostUpdatedTopic, postStream)
feedHandler := sseRouter.AddHandler(FeedUpdatedTopic, feedStream)
allFeedsHandler := sseRouter.AddHandler(FeedUpdatedTopic, allFeedsStream)
r.Route("/api", func(r chi.Router) {
r.Get("/posts/{id}", postHandler)
r.Post("/posts", router.CreatePost)
r.Post("/generate/post", router.GeneratePost)
r.Patch("/posts/{id}", router.UpdatePost)
r.Get("/feeds/{name}", feedHandler)
r.Get("/feeds", allFeedsHandler)
})
go func() {
err = sseRouter.Run(context.Background())
if err != nil {
panic(err)
}
}()
<-sseRouter.Running()
return r
}
type feedSummary struct {
Name string `json:"name"`
Posts int `json:"posts"`
}
type AllFeedsResponse struct {
Feeds []feedSummary `json:"feeds"`
}
type allFeedsStreamAdapter struct {
storage FeedsStorage
logger watermill.LoggerAdapter
}
func (f allFeedsStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {
resp, err := f.getResponse(r)
if err != nil {
logAndWriteError(f.logger, w, err)
return resp, false
}
return resp, true
}
func (f allFeedsStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {
resp, err := f.getResponse(r)
if err != nil {
return resp, false
}
return resp, true
}
func (f allFeedsStreamAdapter) getResponse(r *http.Request) (interface{}, error) {
feeds, err := f.storage.All(r.Context())
if err != nil {
return nil, err
}
response := AllFeedsResponse{
Feeds: []feedSummary{},
}
for _, f := range feeds {
response.Feeds = append(response.Feeds, feedSummary{
Name: f.Name,
Posts: len(f.Posts),
})
}
return response, nil
}
type CreatePostRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
}
func (router Router) CreatePost(w http.ResponseWriter, r *http.Request) {
req := CreatePostRequest{}
err := render.Decode(r, &req)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
post := NewPost(
watermill.NewUUID(),
req.Title,
req.Content,
req.Author,
)
err = router.addPost(r.Context(), post)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
w.WriteHeader(204)
}
func (router Router) GeneratePost(w http.ResponseWriter, r *http.Request) {
title := gofakeit.Sentence(5)
content := gofakeit.Sentence(20)
author := gofakeit.Name()
tagsCount := rand.Intn(3) + 1
for i := 0; i < tagsCount; i++ {
content += fmt.Sprintf(" #%v", generatedTags[rand.Intn(len(generatedTags))])
}
post := NewPost(
watermill.NewUUID(),
title,
content,
author,
)
err := router.addPost(r.Context(), post)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
w.WriteHeader(204)
}
func (router Router) addPost(ctx context.Context, post Post) error {
err := router.PostsStorage.Add(ctx, post)
if err != nil {
return err
}
event := PostCreated{
Post: post,
OccurredAt: time.Now().UTC(),
}
err = router.Publisher.Publish(PostCreatedTopic, event)
if err != nil {
return err
}
return nil
}
type UpdatePostRequest struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
func (router Router) UpdatePost(w http.ResponseWriter, r *http.Request) {
req := UpdatePostRequest{}
err := render.Decode(r, &req)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
post, err := router.PostsStorage.ByID(r.Context(), req.ID)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
newPost := NewPost(
post.ID,
req.Title,
req.Content,
post.Author,
)
err = router.PostsStorage.Update(r.Context(), newPost)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
event := PostUpdated{
OriginalPost: post,
NewPost: newPost,
OccurredAt: time.Now().UTC(),
}
err = router.Publisher.Publish(PostUpdatedTopic, event)
if err != nil {
logAndWriteError(router.Logger, w, err)
return
}
w.WriteHeader(204)
}
type feedStreamAdapter struct {
storage FeedsStorage
logger watermill.LoggerAdapter
}
func (f feedStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {
resp, err := f.getResponse(r)
if err != nil {
logAndWriteError(f.logger, w, err)
return nil, false
}
return resp, true
}
func (f feedStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {
feedUpdated := FeedUpdated{}
err := json.Unmarshal(msg.Payload, &feedUpdated)
if err != nil {
return nil, false
}
feedName := chi.URLParam(r, "name")
if feedUpdated.Name != feedName {
return nil, false
}
resp, err := f.getResponse(r)
if err != nil {
return nil, false
}
return resp, true
}
func (f feedStreamAdapter) getResponse(r *http.Request) (response interface{}, err error) {
feedName := chi.URLParam(r, "name")
feed, err := f.storage.ByName(r.Context(), feedName)
if err != nil {
return nil, err
}
return feed, nil
}
type postStreamAdapter struct {
storage PostsStorage
logger watermill.LoggerAdapter
}
func (p postStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {
resp, err := p.getResponse(r)
if err != nil {
logAndWriteError(p.logger, w, err)
return nil, false
}
return resp, true
}
func (p postStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {
postUpdated := PostUpdated{}
err := json.Unmarshal(msg.Payload, &postUpdated)
if err != nil {
return nil, false
}
postID := chi.URLParam(r, "id")
if postUpdated.OriginalPost.ID != postID {
return nil, false
}
resp, err := p.getResponse(r)
if err != nil {
return nil, false
}
return resp, true
}
func (p postStreamAdapter) getResponse(r *http.Request) (response interface{}, err error) {
postID := chi.URLParam(r, "id")
post, err := p.storage.ByID(r.Context(), postID)
if err != nil {
return nil, err
}
return post, nil
}
func FileServer(r chi.Router, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit URL parameters.")
}
fs := http.StripPrefix(path, http.FileServer(root))
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
})
}
func logAndWriteError(logger watermill.LoggerAdapter, w http.ResponseWriter, err error) {
logger.Error("Error", err, nil)
w.WriteHeader(500)
}