Skip to content

Commit

Permalink
Refactor the app into its own struct
Browse files Browse the repository at this point in the history
  • Loading branch information
theleeeo committed Jan 27, 2024
1 parent 67ab702 commit 785a050
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 110 deletions.
204 changes: 135 additions & 69 deletions app/app.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package app

import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"slices"

Expand All @@ -16,43 +18,15 @@ import (
const (
// staticPath is the path to the static files
staticFilesPath = "./public"
defaultAddress = "localhost:4444"
defaultLogo = "/favicon.ico"
)

func New(cfg *Config, s *server.Server) (*fiber.App, error) {
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine,
GETOnly: true,
})

// Load company logo
logo, err := getCompanyLogo(cfg)
if err != nil {
return nil, err
}

app.Get("/favicon.ico", func(c *fiber.Ctx) error {
return c.Send(logo)
})

// Serve static files
app.Static("/docs", "./docs")
app.Static("/", staticFilesPath)

app.Get("/", createGetIndexHandler(cfg, s))
app.Get("/:version/:role", createRenderDocHandler(cfg, s))

app.Get("/versions", createGetVersionsHandler(s))
app.Get("/version/:version/roles", createGetRolesHandler(s))

return app, nil
}

func getCompanyLogo(cfg *Config) ([]byte, error) {
func getCompanyLogo(location string) ([]byte, error) {
// Check if CompanyLogo is a file
slog.Debug("Checking if CompanyLogo is a file")
// Prepend "public/" to the path because that's where the static files are
file, err := os.Open(fmt.Sprint(staticFilesPath, "/", cfg.CompanyLogo))
file, err := os.Open(fmt.Sprint(staticFilesPath, "/", location))
if err == nil {
slog.Info("Company logo loaded from file")
defer file.Close()
Expand All @@ -65,7 +39,7 @@ func getCompanyLogo(cfg *Config) ([]byte, error) {

// If CompanyLogo is not a file, assume it's a URL and make an HTTP request
slog.Debug("Checking if CompanyLogo is a URL")
resp, err := http.Get(cfg.CompanyLogo)
resp, err := http.Get(location)
if err != nil {
return nil, err
}
Expand All @@ -75,56 +49,148 @@ func getCompanyLogo(cfg *Config) ([]byte, error) {
return io.ReadAll(resp.Body)
}

func createGetIndexHandler(cfg *Config, s *server.Server) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
return c.Render("version-select", fiber.Map{
"CompanyName": cfg.CompanyName,
"CompanyLogo": cfg.CompanyLogo,
}, "layouts/main")
func registerHandlers(a *App) {
a.fiberApp.Get("/favicon.ico", func(c *fiber.Ctx) error {
return c.Send(a.logo)
})

// Serve static files
a.fiberApp.Static("/docs", "./docs")
a.fiberApp.Static("/", staticFilesPath)

a.fiberApp.Get("/", a.getIndexHandler)
a.fiberApp.Get("/:version/:role", a.renderDocHandler)

a.fiberApp.Get("/versions", a.getVersionsHandler)
a.fiberApp.Get("/version/:version/roles", a.getRolesHandler)
}

func validateConfig(cfg *Config) error {
if cfg.Address == "" {
slog.Info("No address set, using default", "Default", defaultAddress)
cfg.Address = defaultAddress
}

if cfg.RootUrl == "" {
return fmt.Errorf("root url is required")
}

rootUrl, err := url.Parse(cfg.RootUrl)
if err != nil {
return fmt.Errorf("invalid root url: %w", err)
}
rootUrl.Scheme = "http"
cfg.RootUrl = rootUrl.String()

if cfg.CompanyName == "" {
return fmt.Errorf("company name is required")
}

if cfg.CompanyLogo == "" {
slog.Info("No company logo set, using default", "Default", defaultLogo)
cfg.CompanyLogo = defaultLogo
}

return nil
}

func createRenderDocHandler(cfg *Config, s *server.Server) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
version := c.Params("version")
role := c.Params("role")
type App struct {
fiberApp *fiber.App

doc := s.GetVersion(version)
if doc == nil {
return c.Status(fiber.StatusNotFound).SendString("404 Version Not Found")
}
cfg *Config
serv *server.Server

logo []byte
}

func New(cfg *Config, s *server.Server) (*App, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}

ok := slices.Contains(doc.Files, role)
if !ok {
return c.Status(fiber.StatusNotFound).SendString("404 Role Not Found")
a := &App{
fiberApp: fiber.New(fiber.Config{
Views: html.New("./views", ".html"),
GETOnly: true,
}),

cfg: cfg,
serv: s,
}

// Load company logo
logo, err := getCompanyLogo(a.cfg.CompanyLogo)
if err != nil {
return nil, err
}
a.logo = logo

registerHandlers(a)

return a, nil
}

func (a *App) Run(ctx context.Context) error {
errChan := make(chan error)
go func() {
slog.Info("starting app", "addr", a.cfg.Address)
if err := a.fiberApp.Listen(a.cfg.Address); err != nil {
errChan <- err
}
}()

return c.Render("doc", fiber.Map{
"Owner": "LeoCorp", //s.Owner(),
"Repo": "LeoRepo", //s.Repo(),
"Path": fmt.Sprintf("%s%s%s", s.Path(), role, s.FileSuffix()),
"Ref": version,
"CompanyName": cfg.CompanyName,
"CompanyLogo": cfg.CompanyLogo,
}, "layouts/main")
select {
case <-ctx.Done():
if err := a.fiberApp.Shutdown(); err != nil {
slog.Error("failed to shutdown app", "error", err)
}
return nil
case err := <-errChan:
return err
}
}

func createGetVersionsHandler(s *server.Server) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
return c.JSON(s.GetVersions())
func (a *App) getIndexHandler(c *fiber.Ctx) error {
return c.Render("version-select", fiber.Map{
"CompanyName": a.cfg.CompanyName,
"CompanyLogo": a.cfg.CompanyLogo,
}, "layouts/main")
}

func (a *App) renderDocHandler(c *fiber.Ctx) error {
version := c.Params("version")
role := c.Params("role")

doc := a.serv.GetVersion(version)
if doc == nil {
return c.Status(fiber.StatusNotFound).SendString("404 Version Not Found")
}

ok := slices.Contains(doc.Files, role)
if !ok {
return c.Status(fiber.StatusNotFound).SendString("404 Role Not Found")
}

return c.Render("doc", fiber.Map{
"RootUrl": a.cfg.RootUrl,
"Path": fmt.Sprintf("%s%s%s", a.serv.Path(), role, a.serv.FileSuffix()),
"Ref": version,
"CompanyName": a.cfg.CompanyName,
"CompanyLogo": a.cfg.CompanyLogo,
}, "layouts/main")
}

func createGetRolesHandler(s *server.Server) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
version := c.Params("version")
func (a *App) getVersionsHandler(c *fiber.Ctx) error {
return c.JSON(a.serv.GetVersions())
}

doc := s.GetVersion(version)
if doc == nil {
return c.Status(fiber.StatusNotFound).SendString("404 Version Not Found")
}
func (a *App) getRolesHandler(c *fiber.Ctx) error {
version := c.Params("version")

return c.JSON(doc.Files)
doc := a.serv.GetVersion(version)
if doc == nil {
return c.Status(fiber.StatusNotFound).SendString("404 Version Not Found")
}

return c.JSON(doc.Files)
}
5 changes: 5 additions & 0 deletions app/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package app

type Config struct {
Address string

// The root url of where to find the swagger files
RootUrl string

CompanyName string
CompanyLogo string
}
5 changes: 4 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ type Config struct {
} `yaml:"github"`
} `yaml:"provider"`

Addr string `yaml:"address"`
App struct {
Address string `yaml:"address"`
RootUrl string `yaml:"root_url"`
} `yaml:"app"`

Server struct {
PollInterval string `yaml:"poll_interval"`
Expand Down
59 changes: 20 additions & 39 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"time"

"github.com/fatih/color"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/theleeeo/docs-server/app"
"github.com/theleeeo/docs-server/provider"
Expand Down Expand Up @@ -66,6 +65,8 @@ func main() {
}

app, err := app.New(&app.Config{
Address: cfg.App.Address,
RootUrl: cfg.App.RootUrl,
CompanyName: cfg.Design.CompanyName,
CompanyLogo: cfg.Design.CompanyLogo,
}, s)
Expand All @@ -75,16 +76,31 @@ func main() {
}

var termChan = make(chan os.Signal, 1)
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
var appErrChan = make(chan error, 1)
var serverErrChan = make(chan error, 1)
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)

ctx, cancel := context.WithCancel(context.Background())

wg := &sync.WaitGroup{}

startApp(ctx, app, cfg.Addr, wg, appErrChan)
startServer(ctx, s, wg, serverErrChan)
go func() {
wg.Add(1)
defer wg.Done()

if err := app.Run(ctx); err != nil {
appErrChan <- err
}
}()

go func() {
wg.Add(1)
defer wg.Done()

if err := s.Run(ctx); err != nil {
serverErrChan <- err
}
}()

select {
case <-termChan:
Expand All @@ -97,7 +113,6 @@ func main() {
}()

cancel()

case err := <-appErrChan:
slog.Error("the app encountered an error", "error", err.Error())
cancel()
Expand All @@ -108,37 +123,3 @@ func main() {

wg.Wait()
}

func startApp(ctx context.Context, app *fiber.App, addr string, wg *sync.WaitGroup, errChan chan<- error) {
go func() {
wg.Add(1)

slog.Info("starting app", "addr", addr)
if err := app.Listen(addr); err != nil {
errChan <- err
}
}()

go func() {
// The waitgroup is freed here because the app.Listen() can finish before the app.Shutdown() is completed.
defer wg.Done()

<-ctx.Done()

if err := app.Shutdown(); err != nil {
slog.Error("failed to shutdown app", "error", err)
}
}()
}

func startServer(ctx context.Context, s *server.Server, wg *sync.WaitGroup, errChan chan<- error) {
go func() {
wg.Add(1)
defer wg.Done()

slog.Info("starting server")
if err := s.Run(ctx); err != nil {
errChan <- err
}
}()
}
2 changes: 2 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ func (s *Server) FileSuffix() string {
}

func (s *Server) Run(ctx context.Context) error {
slog.Info("starting server")

if err := s.Poll(); err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion views/doc.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<redoc spec-url='https://raw.githubusercontent.com/{{ .Owner }}/{{ .Repo }}/{{ .Ref }}/{{ .Path }}'></redoc>
<redoc spec-url='{{ .RootUrl }}/{{ .Ref }}/{{ .Path }}'></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>

0 comments on commit 785a050

Please sign in to comment.