Added an API listener
All checks were successful
Build and Push Docker Image / testing (1.24.x, ubuntu-latest) (push) Successful in 2m30s
Build and Push Docker Image / build-and-push (push) Successful in 11m56s

This commit is contained in:
2025-04-30 18:34:57 -04:00
parent 1475c7911f
commit d01e6a050f
13 changed files with 445 additions and 25 deletions

View File

@ -0,0 +1,14 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/uptrace/bun"
)
var DB *bun.DB
func HomePage(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": "Welcome to the Receipt Tracker API"})
}

View File

@ -0,0 +1,27 @@
package handlers
// form:
import (
"net/http"
"git.savin.nyc/alex/go-receipt-tracker/models"
"github.com/gin-gonic/gin"
)
func GetItems(ctx *gin.Context) {
receiptID := ctx.Param("id")
// Create a slice to store the retrieved tasks
var items models.Items
// Execute the database query to retrieve tasks using Go bun
err := DB.NewSelect().Model(&items).Where("receipt_id = ?", receiptID).Scan(ctx.Request.Context())
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return the retrieved tasks in the response
ctx.JSON(http.StatusOK, gin.H{"items": items})
}

View File

@ -0,0 +1,162 @@
package handlers
// form:
import (
"net/http"
"git.savin.nyc/alex/go-receipt-tracker/models"
"github.com/gin-gonic/gin"
)
func GetReceipts(ctx *gin.Context) {
// Create a slice to store the retrieved tasks
var receipts models.Receipts
// Execute the database query to retrieve tasks using Go bun
err := DB.NewSelect().Model(&receipts).Scan(ctx.Request.Context())
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
for _, receipt := range receipts {
err = DB.NewSelect().Model(&receipt.Items).Where("receipt_id = ?", receipt.ReceiptID).Scan(ctx)
if err != nil {
panic(err)
}
receipt.CreditCard = new(models.CreditCard)
err = DB.NewSelect().Model(receipt.CreditCard).Where("credit_card_id = ?", receipt.CreditCardID).Scan(ctx)
if err != nil {
panic(err)
}
receipt.Merchant = new(models.Merchant)
err = DB.NewSelect().Model(receipt.Merchant).Where("merchant_id = ?", receipt.MerchantID).Scan(ctx)
if err != nil {
panic(err)
}
}
// Return the retrieved tasks in the response
// ctx.JSON(http.StatusOK, gin.H{"receipts": receipts})
ctx.JSON(http.StatusOK, receipts)
}
func GetReceipt(ctx *gin.Context) {
receiptID := ctx.Param("id")
// Check if the task ID is empty
if receiptID == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "ID must be present"})
return
}
receipt := &models.Receipt{}
// Fetch specific record from the database using Go bun
err := DB.NewSelect().Model(receipt).Where("receipt_id = ?", receipt.ReceiptID).Scan(ctx.Request.Context())
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
err = DB.NewSelect().Model(&receipt.Items).Where("receipt_id = ?", receipt.ReceiptID).Scan(ctx)
if err != nil {
panic(err)
}
receipt.CreditCard = new(models.CreditCard)
err = DB.NewSelect().Model(receipt.CreditCard).Where("credit_card_id = ?", receipt.CreditCardID).Scan(ctx)
if err != nil {
panic(err)
}
receipt.Merchant = new(models.Merchant)
err = DB.NewSelect().Model(receipt.Merchant).Where("merchant_id = ?", receipt.MerchantID).Scan(ctx)
if err != nil {
panic(err)
}
if receipt.ReceiptID < 0 {
ctx.JSON(http.StatusNotFound, gin.H{"message": "Receipt not found"})
return
}
ctx.JSON(http.StatusOK, receipt)
}
func UpdateReceipt(ctx *gin.Context) {
receiptID := ctx.Param("id")
if receiptID == "" {
ctx.JSON(http.StatusNoContent, gin.H{"error": "ID must be present"})
return
}
updatedReceipt := &models.Receipt{}
// Bind JSON body to the updatedTask struct
if err := ctx.ShouldBindJSON(updatedReceipt); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update the task record in the database using Go bun
_, err := DB.NewUpdate().Model(updatedReceipt).
Set("title = ?", updatedReceipt.Type).
Where("receipt_id = ?", receiptID).
Exec(ctx.Request.Context())
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"message": "Receipt updated"})
}
func RemoveReceipt(ctx *gin.Context) {
receiptID := ctx.Param("id")
receipt := &models.Receipt{}
// Delete specific task record from the database
res, err := DB.NewDelete().Model(receipt).Where("receipt_id = ?", receiptID).Exec(ctx.Request.Context())
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, err := res.RowsAffected()
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Check if any rows were affected by the delete operation
if rowsAffected > 0 {
ctx.JSON(http.StatusOK, gin.H{"message": "Receipt removed"})
} else {
ctx.JSON(http.StatusNotFound, gin.H{"message": "Receipt not found"})
}
}
func AddReceipt(ctx *gin.Context) {
newReceipt := &models.Receipt{}
if err := ctx.ShouldBindJSON(&newReceipt); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Insert the new task record into the database
_, err := DB.NewInsert().Model(newReceipt).Exec(ctx.Request.Context())
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusCreated, gin.H{"message": "Receipt created"})
}

110
listeners/http_api.go Normal file
View File

@ -0,0 +1,110 @@
package listeners
import (
"context"
"log/slog"
"net/http"
"sync"
"sync/atomic"
"time"
"git.savin.nyc/alex/go-receipt-tracker/listeners/handlers"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/uptrace/bun"
)
const TypeApi = "api"
// HTTPHealthCheck is a listener for providing an HTTP healthcheck endpoint.
type HttpApi struct {
sync.RWMutex
id string // the internal id of the listener
address string // the network address to bind to
config Config // configuration values for the listener
listen *http.Server // the http server
end uint32 // ensure the close methods are only called once
db *bun.DB //
}
// NewHTTPHealthCheck initializes and returns a new HTTP listener, listening on an address.
func NewHttpApi(config Config, db *bun.DB) *HttpApi {
return &HttpApi{
id: config.ID,
address: config.Address,
config: config,
db: db,
}
}
// ID returns the id of the listener.
func (l *HttpApi) ID() string {
return l.id
}
// Address returns the address of the listener.
func (l *HttpApi) Address() string {
return l.address
}
// Protocol returns the address of the listener.
func (l *HttpApi) Protocol() string {
if l.listen != nil && l.listen.TLSConfig != nil {
return "https"
}
return "http"
}
// Init initializes the listener.
func (l *HttpApi) Init(_ *slog.Logger) error {
handlers.DB = l.db
router := gin.Default()
router.Use(cors.Default())
v1 := router.Group("/api/v1")
{
v1.GET("/", handlers.HomePage)
v1.GET("/receipts", handlers.GetReceipts)
v1.GET("/receipts/:id", handlers.GetReceipt)
v1.DELETE("/receipts/:id", handlers.RemoveReceipt)
v1.POST("/receipts", handlers.AddReceipt)
v1.PUT("/receipts/:id", handlers.UpdateReceipt)
v1.GET("/receipts/:id/items", handlers.GetItems)
}
l.listen = &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
Addr: l.address,
Handler: router.Handler(),
}
if l.config.TLSConfig != nil {
l.listen.TLSConfig = l.config.TLSConfig
}
return nil
}
// Serve starts listening for new connections and serving responses.
func (l *HttpApi) Serve() {
if l.listen.TLSConfig != nil {
_ = l.listen.ListenAndServeTLS("", "")
} else {
_ = l.listen.ListenAndServe()
}
}
// Close closes the listener and any client connections.
func (l *HttpApi) Close() {
l.Lock()
defer l.Unlock()
if atomic.CompareAndSwapUint32(&l.end, 0, 1) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = l.listen.Shutdown(ctx)
}
}

View File

@ -9,8 +9,10 @@ import (
// Config contains configuration values for a listener.
type Config struct {
// TLSConfig is a tls.Config configuration to be used with the listener.
// See examples folder for basic and mutual-tls use.
Type string
ID string
Address string
// TLSConfig is a tls.Config configuration to be used with the listener. See examples folder for basic and mutual-tls use.
TLSConfig *tls.Config
}