Added: downloading and returning wikipedia articles

This commit is contained in:
Oliwier Adamczyk
2025-10-04 23:14:41 +02:00
parent f542f01b49
commit 6df63dc4c1
26 changed files with 636 additions and 100 deletions

78
api/article/handler.go Normal file
View File

@@ -0,0 +1,78 @@
package article
import (
"net/http"
"scrap/api/httpio"
"scrap/internal/article"
"scrap/internal/db"
)
func ArticleDownloadHandler(w http.ResponseWriter, r *http.Request) {
dbInstance := db.GetInstance()
txRepo := db.NewTxRepository(dbInstance)
articleRepo := article.NewArticleRepository()
service := article.NewArticleService(txRepo, articleRepo)
if err := service.DownloadArticles(); err != nil {
switch err {
default:
httpio.RaiseOnlyStatusCode(w, http.StatusInternalServerError)
}
return
}
}
func ArticleQueryHandler(w http.ResponseWriter, r *http.Request) {
body, err := httpio.ParseURLQuery[ArticleQueryRequest](
r,
httpio.URLQueryKey[string]("title"),
)
if err != nil {
httpio.RaiseOnlyStatusCode(w, http.StatusInternalServerError)
return
}
if httpErr := body.Validate(); httpErr != nil {
httpErr.Raise(w)
return
}
dbInstance := db.GetInstance()
txRepo := db.NewTxRepository(dbInstance)
articleRepo := article.NewArticleRepository()
service := article.NewArticleService(txRepo, articleRepo)
articleQueryData := article.ArticleQueryDTO{
Title: body.Title,
}
articles, err := service.QueryArticles(articleQueryData)
if err != nil {
switch err {
case article.ErrArticleTitleInvalidLength:
ErrHttpArticleTitleInvalidLength.Raise(w)
default:
httpio.RaiseOnlyStatusCode(w, http.StatusInternalServerError)
}
return
}
articlesOut := make([]ArticleResponse, 0, len(articles))
for _, a := range articles {
ar := ArticleResponse{
Uuid: a.Uuid,
Title: a.Title,
Content: a.Content,
}
articlesOut = append(articlesOut, ar)
}
if err = ArticleQueryResponse(articlesOut).Return(w, http.StatusOK); err != nil {
httpio.RaiseOnlyStatusCode(w, http.StatusInternalServerError)
return
}
}

14
api/article/httperror.go Normal file
View File

@@ -0,0 +1,14 @@
package article
import (
"net/http"
"scrap/api/httpio"
)
var (
ErrHttpArticleTitleInvalidLength = httpio.HTTPError{
StatusCode: http.StatusBadRequest,
ErrorCode: "ARTICLE_TITLE_LENGTH",
Message: "Invalid title length.",
}
)

16
api/article/request.go Normal file
View File

@@ -0,0 +1,16 @@
package article
import "scrap/api/httpio"
type ArticleQueryRequest struct {
Title string `json:"title"`
}
func (a ArticleQueryRequest) Validate() *httpio.HTTPError {
titleLength := len(a.Title)
if titleLength < 1 || titleLength > 255 {
return &ErrHttpArticleTitleInvalidLength
}
return nil
}

15
api/article/response.go Normal file
View File

@@ -0,0 +1,15 @@
package article
import "scrap/api/httpio"
type ArticleResponse struct {
Uuid string `json:"uuid"`
Title string `json:"title"`
Content string `json:"content"`
}
func ArticleQueryResponse(articles []ArticleResponse) httpio.ResponseIO {
return httpio.ResponseIO{
"articles": articles,
}
}

24
api/httpio/httperror.go Normal file
View File

@@ -0,0 +1,24 @@
package httpio
import (
"encoding/json"
"net/http"
)
type HTTPError struct {
StatusCode int `json:"-"`
ErrorCode string `json:"error-code"`
Message string `json:"message"`
}
func (h HTTPError) Raise(w http.ResponseWriter) {
jsonBytes, _ := json.Marshal(h)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(h.StatusCode)
w.Write(jsonBytes)
}
func RaiseOnlyStatusCode(w http.ResponseWriter, code int) {
http.Error(w, "", code)
}

38
api/httpio/request.go Normal file
View File

@@ -0,0 +1,38 @@
package httpio
import (
"encoding/json"
"errors"
"io"
"log"
"net/http"
)
type IRequestIO interface {
// Validates the received request.
Validate() *HTTPError
}
// Parses request body into the provided struct.
// Throws an error if the body could not be parsed.
func ParseRequestBody[T IRequestIO](r *http.Request) (*T, error) {
requestBytes, err := io.ReadAll(r.Body)
if err != nil {
log.Println(err.Error())
return nil, err
}
if !json.Valid(requestBytes) {
return nil, errors.New("invalid JSON format")
}
var req T
err = json.Unmarshal(requestBytes, &req)
if err != nil {
log.Println(err.Error())
return nil, err
}
return &req, nil
}

21
api/httpio/response.go Normal file
View File

@@ -0,0 +1,21 @@
package httpio
import (
"encoding/json"
"net/http"
)
type ResponseIO map[string]any
func (r ResponseIO) Return(w http.ResponseWriter, statusCode int) error {
jsonBytes, err := json.Marshal(r)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
w.Write(jsonBytes)
return nil
}

86
api/httpio/urlquery.go Normal file
View File

@@ -0,0 +1,86 @@
package httpio
import (
"encoding/json"
"errors"
"net/http"
"strconv"
)
type URLQueryValueType interface {
string | int | float32 | float64 | bool
}
type iURLQueryKeyType interface {
GetKey() string
}
type URLQueryKeyType[T URLQueryValueType] struct {
Key string
_ T
}
func (u URLQueryKeyType[T]) GetKey() string { return u.Key }
func URLQueryKey[T URLQueryValueType](key string) iURLQueryKeyType {
return URLQueryKeyType[T]{
Key: key,
}
}
func ParseURLQuery[T IRequestIO](r *http.Request, keys ...iURLQueryKeyType) (*T, error) {
query := make(map[string]any, len(keys))
for _, key := range keys {
queryValue := r.URL.Query().Get(key.GetKey())
if queryValue == "" {
continue
}
switch key.(type) {
case URLQueryKeyType[string]:
query[key.GetKey()] = queryValue
case URLQueryKeyType[int]:
x, err := strconv.Atoi(queryValue)
if err != nil {
return nil, err
}
query[key.GetKey()] = x
case URLQueryKeyType[float32]:
x, err := strconv.ParseFloat(queryValue, 32)
if err != nil {
return nil, err
}
query[key.GetKey()] = x
case URLQueryKeyType[float64]:
x, err := strconv.ParseFloat(queryValue, 64)
if err != nil {
return nil, err
}
query[key.GetKey()] = x
case URLQueryKeyType[bool]:
x, err := strconv.ParseBool(queryValue)
if err != nil {
return nil, err
}
query[key.GetKey()] = x
default:
return nil, errors.New("unsupported URL query key type")
}
}
queryBytes, _ := json.Marshal(query)
var req T
err := json.Unmarshal(queryBytes, &req)
if err != nil {
return nil, err
}
return &req, nil
}

17
api/setup.go Normal file
View File

@@ -0,0 +1,17 @@
package api
import (
"net/http"
"scrap/api/article"
"github.com/go-chi/chi"
)
func Setup() {
r := chi.NewRouter()
r.Get("/articles", article.ArticleQueryHandler)
r.Get("/articles-download", article.ArticleDownloadHandler)
http.ListenAndServe(":8080", r)
}