Skip to content

Commit

Permalink
init setup with log, config, erros, server
Browse files Browse the repository at this point in the history
  • Loading branch information
spy16 committed Jul 5, 2022
1 parent 789644d commit 7eb514f
Show file tree
Hide file tree
Showing 19 changed files with 1,524 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
#

.idea/
.vscode/
bin/
expt/
dist/
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 🌔 Moonshot

A boilerplate Go library for quickly setting up your next moonshot idea!

## Features

* Config management
* Create struct, pass its pointer to `moonshot.App`.
* Moonshot will take care of loading configs from environment/files.
* File can be overriden by `--config` flag also.
* You can run `./myapp configs` to see the actual loaded configs.
* HTTP Server setup
* HTTP server is pre-configured with graceful shutdown enabled.
* Server is pre-configured with handlers for `/health`, NotFound, MethodNotAllowed.
* Panic recovery is enabled.
* You can set the `Routes` field in `moonshot.App` to add custom routes or override.
* Errors package
* An easy-to-use errors package with common category of errors pre-defined.
* Just do `errors.ErrInvalid.WithMsgf()` or `WithCausef()` to add additional context.
* Logging
* `log` package is automatically configured based on `--log-level` and `--log-format` flags.
* Pass log-context using `log.Inject(ctx, fields)`

## Usage

1. Create `main.go`.
2. Initiailise `moonshot.App`:

```go
package main

import "github.com/spy16/moonshot"

var myConfig struct {
Database string `mapstructure:"database"`
}

func main() {
app := moonshot.App{
Name: "myapp",
Short: "MyApp does cool things",
CfgPtr: &myConfig,
Routes: func(r *chi.Mux) {
r.Get("/", myAppHomePageHandler)
},
}

os.Exit(app.Launch())
}
```
3. Build the app `go build -o myapp main.go`
4. Run the app:

```shell
$ ./myapp --help
MyApp does cool things
Usage:
myapp [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
configs Show currently loaded configurations
help Help about any command
serve Start HTTP server.
Flags:
-c, --config string Config file path override
-h, --help help for moonshot-demo
Use "moonshot-demo [command] --help" for more information about a command.
```

* You can run `./myapp serve --addr="localhost:8080"` for starting server.
* You can pass `--static-dir` and `--static-route` flags to `serve` command for serving static files.

> **Note**: Refer `./_example` for a demo application.
39 changes: 39 additions & 0 deletions _example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"context"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/go-chi/chi"

"github.com/spy16/moonshot"
"github.com/spy16/moonshot/httputils"
)

var appCfg struct {
Addr string `mapstructure:"addr" yaml:"addr" json:"addr"`
LogLevel string `mapstructure:"log_level" yaml:"log_level" json:"log_level"`
LogFormat string `mapstructure:"log_format" yaml:"log_format" json:"log_format"`
}

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

app := &moonshot.App{
Name: "moonshot-demo",
Short: "A sample moonshot app setup",
CfgPtr: &appCfg,
Routes: func(r *chi.Mux) {
// set up any custom routes here.
r.Get("/hello", func(wr http.ResponseWriter, req *http.Request) {
httputils.Respond(wr, req, http.StatusOK, "Hello!")
})
},
}

os.Exit(app.Launch(ctx))
}
1 change: 1 addition & 0 deletions _example/moonshot-demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addr: ":8080"
32 changes: 32 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Config

A convenience wrapper around `viper` that provides easy-to-use struct-based config loading.

## Example

### Load Directly

```golang
package main

func main() {
var cfg Config
opts := []config.Option{
config.WithEnv(),
}
if err := config.Load(&cfg, opts...); err != nil {
panic(err)
}

fmt.Println(cfg)
}

type Config struct {
Addr string `default:":8080"`
StatsD struct {
Host string `default:"localhost"`
Port int `default:"8125"`
}
}
```

1 change: 1 addition & 0 deletions config/cobra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package config
171 changes: 171 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package config

import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"

"github.com/mcuadros/go-defaults"
"github.com/spf13/viper"
)

// Load loads configurations into the given structPtr.
func Load(structPtr interface{}, opts ...Option) error {
l := &viperLoader{
viper: viper.New(),
intoPtr: structPtr,
useDefaults: true,
}

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

return l.load()
}

type viperLoader struct {
viper *viper.Viper
configs []configDef
intoPtr interface{}
confFile string
confName string
useEnv bool
envPrefix string
useDefaults bool
}

func (l *viperLoader) load() error {
v := l.viper

keys, err := extractConfigDefs(l.intoPtr, l.useDefaults)
if err != nil {
return err
}

for _, cfg := range keys {
v.SetDefault(cfg.Key, cfg.Default)
}

if l.useEnv {
// for transforming app.host to app_host
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
v.SetEnvPrefix(l.envPrefix)
v.AutomaticEnv()
for _, cfg := range keys {
if err := v.BindEnv(cfg.Key); err != nil {
return err
}
}
}

if l.confFile != "" {
v.SetConfigFile(l.confFile)
if err := v.ReadInConfig(); err != nil {
return err
}
} else {
if l.confName == "" {
l.confName = "config"
}
v.AddConfigPath("./")
v.AddConfigPath(getExecPath())
v.SetConfigName(l.confName)
_ = v.ReadInConfig()
}

return v.Unmarshal(l.intoPtr)
}

type configDef struct {
Key string `json:"key"`
Doc string `json:"doc"`
Default interface{} `json:"default"`
}

func extractConfigDefs(structPtr interface{}, useDefaults bool) ([]configDef, error) {
rv := reflect.ValueOf(structPtr)

if err := ensureStructPtr(rv); err != nil {
return nil, err
}

if useDefaults {
defaults.SetDefaults(structPtr)
}

return readRecursive(deref(rv), "")
}

func readRecursive(rv reflect.Value, rootKey string) ([]configDef, error) {
rt := rv.Type()

var acc []configDef
for i := 0; i < rv.NumField(); i++ {
ft := rt.Field(i)
fv := deref(rv.Field(i))

key := toCamelCase(ft.Name)
if rootKey != "" {
key = fmt.Sprintf("%s.%s", rootKey, key)
}

if fv.Kind() == reflect.Struct {
nestedConfigs, err := readRecursive(fv, key)
if err != nil {
return nil, err
}
acc = append(acc, nestedConfigs...)
} else {
acc = append(acc, configDef{
Key: key,
Doc: ft.Tag.Get("doc"),
Default: fv.Interface(),
})
}
}

return acc, nil
}

func toCamelCase(s string) string {
var result string
for i, r := range s {
if i > 0 && (r >= 'A' && r < 'Z') {
result += "_"
}
result += strings.ToLower(string(r))
}
return result
}

func deref(rv reflect.Value) reflect.Value {
if rv.Kind() == reflect.Ptr {
rv = reflect.Indirect(rv)
}
return rv
}

func ensureStructPtr(value reflect.Value) error {
if value.Kind() != reflect.Ptr {
return fmt.Errorf("need a pointer to struct, not '%s'", value.Kind())
} else {
value = reflect.Indirect(value)
if value.Kind() != reflect.Struct {
return fmt.Errorf("need a pointer to struct, not pointer to '%s'", value.Kind())
}
}
return nil
}

func getExecPath() string {
execPath, err := os.Executable()
if err != nil {
return ""
}
return filepath.Dir(execPath)
}
31 changes: 31 additions & 0 deletions config/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package config

import (
"strings"
)

type Option func(l *viperLoader) error

func WithEnv(prefix ...string) Option {
return func(l *viperLoader) error {
l.useEnv = true
if len(prefix) > 0 {
l.envPrefix = strings.TrimSpace(prefix[0])
}
return nil
}
}

func WithName(name string) Option {
return func(l *viperLoader) error {
l.confName = strings.TrimSpace(name)
return nil
}
}

func WithFile(filePath string) Option {
return func(l *viperLoader) error {
l.confFile = filePath
return nil
}
}
Loading

0 comments on commit 7eb514f

Please sign in to comment.