Skip to content

Commit

Permalink
feat: Bootstrap http server for API (#18)
Browse files Browse the repository at this point in the history
* refactor: Change ID to be a string

* refactor: Reorganise code layout

* feat: Server implements ServerInterfaces

* feat: Add server start to ServerCommand

* fix: linting issue
  • Loading branch information
MaikelVeen committed Jun 19, 2024
1 parent f4656ca commit fd07428
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 36 deletions.
3 changes: 0 additions & 3 deletions api/api.go

This file was deleted.

6 changes: 0 additions & 6 deletions api/oapi-codegen-server.yaml

This file was deleted.

18 changes: 15 additions & 3 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"log/slog"
"os"

"github.com/glass-cms/glasscms/ctx"
"github.com/glass-cms/glasscms/server"
"github.com/lmittmann/tint"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -36,7 +38,17 @@ func NewServerCommand() *ServerCommand {
return sc
}

func (c *ServerCommand) Execute(_ *cobra.Command, _ []string) error {
c.logger.Info("Starting server")
return nil
func (c *ServerCommand) Execute(cmd *cobra.Command, _ []string) error {
server, err := server.New(c.logger)
if err != nil {
return err
}

_ = ctx.SigtermCacellationContext(cmd.Context(), func() {
c.logger.Info("shutting down server")
server.Shutdown()
})

c.logger.Info("starting server")
return server.ListenAndServer()
}
5 changes: 5 additions & 0 deletions codegen-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package: server
generate:
std-http-server: true
models: true
output: server/server.gen.go
25 changes: 25 additions & 0 deletions ctx/sigterm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ctx

import (
"context"
"os"
"os/signal"
"syscall"
)

// SigtermCacellationContext returns a new context that is canceled when an
// interrupt signal (SIGTERM or SIGINT) is received.
// Additionaly invokes the provided onCancel function before canceling the context.
func SigtermCacellationContext(ctx context.Context, onCancel func()) context.Context {
ctx, cancel := context.WithCancel(ctx)

interruptCh := make(chan os.Signal, 1)
signal.Notify(interruptCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)

go func() {
<-interruptCh
onCancel()
cancel()
}()
return ctx
}
3 changes: 3 additions & 0 deletions generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package main

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen-server.yaml openapi.yaml
19 changes: 11 additions & 8 deletions item/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ package item
import "time"

const (
// PropertyTitle is the key for the title property.
PropertyTitle = "title"
)

type Item struct {
Name string `json:"name" yaml:"name"`
Path string `json:"path" yaml:"path"`
Content string `json:"content" yaml:"content"`
Hash string `json:"hash" yaml:"hash"`
CreateTime time.Time `json:"createTime" yaml:"createTime"`
UpdateTime time.Time `json:"updateTime" yaml:"updateTime"`
Properties map[string]any `json:"properties" yaml:"properties"`
UID string `json:"uid" yaml:"uid"`
// Name is the full resource name of the item.
// Format: collections/{collection}/items/{item}
Name string `json:"name" yaml:"name"`
DisplayName string `json:"display_name" yaml:"display_name"`
Path string `json:"path" yaml:"path"`
Content string `json:"content" yaml:"content"`
Hash string `json:"hash" yaml:"hash"`
CreateTime time.Time `json:"create_time" yaml:"create_time"`
UpdateTime time.Time `json:"update_time" yaml:"update_time"`
Properties map[string]any `json:"properties" yaml:"properties"`
}

// Title returns the title property of the item if it exists.
Expand Down
14 changes: 8 additions & 6 deletions api/openapi.yaml → openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,30 @@ components:
required:
- id
- name
- display_name
- path
- content
- created_time
- create_time
- update_time
- props
- properties
properties:
id:
type: integer
format: int64
type: string
name:
type: string
display_name:
type: string
path:
type: string
content:
type: string
created_time:
create_time:
type: string
format: date-time
update_time:
type: string
format: date-time
props:
properties:
type: object
additionalProperties: {}
description: Item represents an individual content item.
4 changes: 2 additions & 2 deletions scripts/compile-spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
# See: https://github.com/microsoft/typespec/issues/3397.

current_dir=$(pwd)
cd api/typespec || exit
tsp compile . --emit @typespec/openapi3 --option "@typespec/openapi3.emitter-output-dir=${current_dir}/api"
cd typespec || exit
tsp compile . --emit @typespec/openapi3 --option "@typespec/openapi3.emitter-output-dir=${current_dir}"
cd "$current_dir" || exit
23 changes: 23 additions & 0 deletions server/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package server

import (
"errors"
"fmt"
)

type Option func(*Server) error

// WithPort is an option that sets the port the server listens on.
func WithPort(port string) func(*Server) error {
return func(s *Server) error {
if port == "" {
return errors.New("port cannot be empty")
}

if s.server != nil {
s.server.Addr = fmt.Sprintf(":%s", port)
}

return nil
}
}
11 changes: 6 additions & 5 deletions api/server.gen.go → server/server.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package server

import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
)

const (
ShutdownGracePeriod = 10 * time.Second
DefaultPort = 8080
DefaultReadTimeout = 5 * time.Second
DefaultWriteTimeout = 10 * time.Second
)

type Server struct {
logger *slog.Logger
server *http.Server
}

var _ ServerInterface = (*Server)(nil)

func New(
logger *slog.Logger,
opts ...Option,
) (*Server, error) {
server := &Server{
logger: logger,
}

server.server = &http.Server{
Handler: HandlerFromMux(server, http.NewServeMux()),
Addr: fmt.Sprintf(":%v", DefaultPort),
ReadTimeout: DefaultReadTimeout,
WriteTimeout: DefaultWriteTimeout,
}

for _, opt := range opts {
if err := opt(server); err != nil {
return nil, err
}
}

return server, nil
}

// ListenAndServe starts the server.
func (s *Server) ListenAndServer() error {
s.logger.Info("server is listening on :8080")
return s.server.ListenAndServe()
}

// Shutdown gracefully shuts down the underlying server without interrupting any active connections.
func (s *Server) Shutdown() {
ctx, cancel := context.WithTimeout(context.Background(), ShutdownGracePeriod)
defer cancel()

if err := s.server.Shutdown(ctx); err != nil {
s.logger.Error("could not gracefully shutdown the server:", "err", err)
return
}

s.logger.Info("server stopped")
}

func (s *Server) ItemsDelete(w http.ResponseWriter, _ *http.Request) {
// TODO.
w.WriteHeader(http.StatusNotImplemented)
}

func (s *Server) ItemsList(w http.ResponseWriter, _ *http.Request) {
// TODO.
w.WriteHeader(http.StatusNotImplemented)
}

func (s *Server) ItemsCreate(w http.ResponseWriter, _ *http.Request) {
// TODO.
w.WriteHeader(http.StatusNotImplemented)
}
File renamed without changes.
7 changes: 4 additions & 3 deletions api/typespec/main.tsp → typespec/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ namespace Items {

@doc("Item represents an individual content item.")
model Item {
id: int64;
id: string;
name: string;
display_name: string;
path: string;
content: string;
created_time: utcDateTime;
create_time: utcDateTime;
update_time: utcDateTime;
props: Record<unknown>;
properties: Record<unknown>;
}
File renamed without changes.
File renamed without changes.

0 comments on commit fd07428

Please sign in to comment.