-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
init setup with log, config, erros, server
- Loading branch information
Showing
19 changed files
with
1,524 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
addr: ":8080" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.