Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: go build

on:
push:
branches: [ "master" ]
branches: [ "main" ]
pull_request:
branches: [ "master" ]
branches: [ "main" ]

jobs:

Expand Down Expand Up @@ -32,7 +32,7 @@ jobs:
run: go build -v ./...

- name: Lint
uses: golangci/golangci-lint-action@v3.3.1
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout 5m
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# qr-coder
[![codecov](https://codecov.io/gh/etilite/qr-coder/graph/badge.svg?token=A70ZRV50JV)](https://codecov.io/gh/etilite/qr-coder)

Microservice to generate QR-codes in png format.
Text content is encoded in UTF-8.

By default, `qr-coder` adds to content `UTF-8 BOM` prefix for better unicode-compatibility with scanners.

## Usage
### Build project
Expand Down
48 changes: 5 additions & 43 deletions cmd/qr-coder/main.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,25 @@
package main

import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"time"

"github.com/etilite/qr-coder/internal/app"
httpserver "github.com/etilite/qr-coder/internal/delivery/http"
)

const (
shutdownTime time.Duration = 10 * time.Second
)

func main() {
cfg := app.NewConfigFromEnv()
ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer done()

a := app.New(cfg)
cfg := app.NewConfigFromEnv()

server := httpserver.NewServer(cfg.HTTPAddr)

if err := a.Run(server); err != nil {
if err := server.Run(ctx); err != nil {
slog.Error("unable to start app", "error", err)
panic(err)
}

shutdown := make(chan bool)
gracefulStop(shutdown)
<-shutdown
slog.Info("signal caught, shutting down")

go aggressiveStop()

if err := a.Stop(); err != nil {
slog.Error("unable to stop app", "error", err)
panic(err)
}
}

func gracefulStop(shutdown chan<- bool) {
c := make(chan os.Signal)
signal.Notify(c,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)

go func() {
<-c
shutdown <- true
}()
}

func aggressiveStop() {
ticker := time.NewTicker(shutdownTime)
<-ticker.C

slog.Warn("application is aggressive stopped")
os.Exit(0)
}
44 changes: 0 additions & 44 deletions internal/app/app.go

This file was deleted.

12 changes: 5 additions & 7 deletions internal/coder/coder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package coder
import (
"fmt"

"github.com/etilite/qr-coder/internal/model"
"github.com/skip2/go-qrcode"
)

const utf8BOM = "\ufeff"

type QRCode struct {
Content string `json:"content"`
Size int `json:"size"`
}
const (
utf8BOM = "\ufeff"
)

func (code *QRCode) Generate() ([]byte, error) {
func Encode(code model.QRCode) ([]byte, error) {
qrCode, err := qrcode.Encode(utf8BOM+code.Content, qrcode.Medium, code.Size)
if err != nil {
return nil, fmt.Errorf("could not generate a QR code: %v", err)
Expand Down
77 changes: 77 additions & 0 deletions internal/coder/coder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package coder

import (
"testing"

"github.com/etilite/qr-coder/internal/model"
"github.com/stretchr/testify/assert"
)

func TestQRCode_Generate(t *testing.T) {
t.Parallel()

t.Run("success", func(t *testing.T) {
t.Parallel()

code := model.QRCode{
Content: "test",
Size: 32,
}

got, err := Encode(code)

assert.NoError(t, err)
assert.NotEmpty(t, got)
})

t.Run("encode error", func(t *testing.T) {
t.Parallel()

code := model.QRCode{
Content: tooBigContent,
Size: 32,
}

got, err := Encode(code)

assert.Error(t, err)
assert.Empty(t, got)
})
}

const tooBigContent = `this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_
this_content_is_too_big_this_content_is_too_big_this_content_is_too_big_`
52 changes: 34 additions & 18 deletions internal/delivery/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,56 @@ package http
import (
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"

"github.com/etilite/qr-coder/internal/coder"
"github.com/etilite/qr-coder/internal/model"
)

type QRCodeHandler struct {
encode func(code model.QRCode) ([]byte, error)
}

func (h *QRCodeHandler) handle() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
qrCode := coder.QRCode{}
err := json.NewDecoder(request.Body).Decode(&qrCode)
func NewQRCodeHandler() *QRCodeHandler {
return &QRCodeHandler{encode: coder.Encode}
}

//writer.Header().Set("Content-Type", "application/json")
func (h *QRCodeHandler) handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := r.Body.Close(); err != nil {
slog.Error("handler: failed to close request body", "error", err)
}
}()

qrCode := model.QRCode{}

if err := json.NewDecoder(r.Body).Decode(&qrCode); err != nil {
err = fmt.Errorf("failed to decode JSON: %v", err)
slog.Error("handler: bad request", "error", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if err != nil {
err = fmt.Errorf("failed to decode JSON: %w", err)
log.Print(err)
http.Error(writer, err.Error(), http.StatusBadRequest)
if err := qrCode.Validate(); err != nil {
err = fmt.Errorf("failed to validate QR Code: %v", err)
slog.Error("handler: bad request", "error", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

//var image []byte
image, err := qrCode.Generate()
image, err := h.encode(qrCode)
if err != nil {
writer.WriteHeader(400)
json.NewEncoder(writer).Encode(
fmt.Sprintf("Could not generate QR code. %v", err),
)
err = fmt.Errorf("failed to generate QR-code: %v", err)
slog.Error("handler: internal error", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

writer.Header().Set("Content-Type", "image/png")
writer.Write(image)
w.Header().Set("Content-Type", "image/png")
if _, err := w.Write(image); err != nil {
slog.Error("handler: failed to write response", "error", err)
}
}
}
Loading