mirror of https://github.com/veonik/squircy3
Add static linking of extra plugins, other updates
parent
d39606f7e9
commit
4a00f26c1f
|
@ -2,8 +2,7 @@
|
|||
!.gitignore
|
||||
!.dockerignore
|
||||
out/*
|
||||
node_modules/*
|
||||
testdata/node_modules/*
|
||||
node_modules
|
||||
testdata/yarn-error.log
|
||||
testdata/yarn.lock
|
||||
yarn.lock
|
||||
|
|
24
Dockerfile
24
Dockerfile
|
@ -1,6 +1,10 @@
|
|||
FROM golang:buster AS build
|
||||
FROM golang:alpine AS build
|
||||
|
||||
ARG race
|
||||
ARG plugin_type=shared
|
||||
|
||||
RUN apk update && \
|
||||
apk add yarn alpine-sdk upx
|
||||
|
||||
WORKDIR /squircy
|
||||
|
||||
|
@ -11,19 +15,13 @@ RUN go get -v github.com/gobuffalo/packr/v2/... && \
|
|||
|
||||
RUN go get -v ./...
|
||||
|
||||
RUN make clean all RACE=${race}
|
||||
RUN make clean dist RACE=${race} PLUGIN_TYPE=${plugin_type}
|
||||
|
||||
|
||||
FROM debian:buster-slim
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y ca-certificates curl gnupg
|
||||
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y yarn
|
||||
RUN apk update && \
|
||||
apk add ca-certificates curl gnupg yarn
|
||||
|
||||
COPY config.toml.dist /home/squircy/.squircy/config.toml
|
||||
|
||||
|
@ -32,7 +30,7 @@ COPY package.json /home/squircy/.squircy/scripts/package.json
|
|||
RUN cd /home/squircy/.squircy/scripts && \
|
||||
yarn install
|
||||
|
||||
RUN useradd -d /home/squircy squircy
|
||||
RUN adduser -D -h /home/squircy squircy
|
||||
|
||||
RUN chown -R squircy: /home/squircy
|
||||
|
||||
|
@ -40,7 +38,7 @@ USER squircy
|
|||
|
||||
WORKDIR /squircy
|
||||
|
||||
COPY --from=build /squircy/out/squircy /bin/squircy
|
||||
COPY --from=build /squircy/out/squircy_linux_amd64 /bin/squircy
|
||||
|
||||
COPY --from=build /squircy/out/*.so /squircy/plugins/
|
||||
|
||||
|
|
71
Makefile
71
Makefile
|
@ -3,36 +3,61 @@
|
|||
|
||||
SUBPACKAGES := cli config event irc plugin vm
|
||||
|
||||
PLUGINS := $(patsubst plugins/%,%,$(wildcard plugins/*))
|
||||
PLUGINS ?= $(patsubst plugins/%,%,$(wildcard plugins/*))
|
||||
|
||||
SOURCES := $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES)))
|
||||
GENERATOR_SOURCES := $(wildcard cmd/squircy/defconf/*)
|
||||
|
||||
OUTPUT_BASE := out
|
||||
|
||||
PLUGIN_TARGETS := $(patsubst %,$(OUTPUT_BASE)/%.so,$(PLUGINS))
|
||||
SQUIRCY_TARGET := $(OUTPUT_BASE)/squircy
|
||||
|
||||
RACE ?= -race
|
||||
RACE ?= -race
|
||||
TEST_ARGS ?= -count 1
|
||||
|
||||
# Include PLUGIN_TYPE=linked in the command-line when invoking make to link
|
||||
# extra plugins directly in the main binary rather than generating shared object files.
|
||||
PLUGIN_TYPE ?= shared
|
||||
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
GOARM ?= $(shell go env GOARM)
|
||||
CC ?= $(shell go env CC)
|
||||
PACKR ?= $(shell which packr || which packr2)
|
||||
|
||||
SQUIRCY_TARGET := $(OUTPUT_BASE)/squircy
|
||||
SQUIRCY_DIST := $(SQUIRCY_TARGET)_$(GOOS)_$(GOARCH)$(GOARM)
|
||||
PLUGIN_DIST := $(patsubst %,$(OUTPUT_BASE)/%_$(GOOS)_$(GOARCH)$(GOARM).so,$(PLUGINS))
|
||||
|
||||
ifeq ($(PLUGIN_TYPE),linked)
|
||||
PLUGIN_TARGETS :=
|
||||
EXTRA_TAGS := -tags linked_plugins
|
||||
DIST_TARGETS := $(SQUIRCY_DIST)
|
||||
LINKED_PLUGINS_FILE := cmd/squircy/linked_plugins.go
|
||||
else
|
||||
PLUGIN_TARGETS := $(patsubst %,$(OUTPUT_BASE)/%.so,$(PLUGINS))
|
||||
EXTRA_TAGS :=
|
||||
DIST_TARGETS := $(SQUIRCY_DIST) $(PLUGIN_DIST)
|
||||
LINKED_PLUGINS_FILE := cmd/squircy/shared_plugins.go
|
||||
endif
|
||||
|
||||
TESTDATA_NODEMODS_TARGET := testdata/node_modules
|
||||
|
||||
.PHONY: all build generate run plugins clean test
|
||||
SQUIRCY3_VERSION := $(if $(shell test -d .git && echo "1"),$(shell git describe --always --tags),SNAPSHOT)
|
||||
|
||||
.PHONY: all build run plugins clean test dist
|
||||
|
||||
all: build plugins
|
||||
|
||||
clean:
|
||||
cd cmd/squircy && \
|
||||
packr2 clean
|
||||
$(PACKR) clean
|
||||
rm -rf $(OUTPUT_BASE)
|
||||
|
||||
build: generate $(SQUIRCY_TARGET)
|
||||
|
||||
generate: $(OUTPUT_BASE)/.generated
|
||||
build: $(SQUIRCY_TARGET)
|
||||
|
||||
plugins: $(PLUGIN_TARGETS)
|
||||
|
||||
dist: $(DIST_TARGETS)
|
||||
|
||||
run: build
|
||||
$(SQUIRCY_TARGET)
|
||||
|
||||
|
@ -45,19 +70,31 @@ $(TESTDATA_NODEMODS_TARGET):
|
|||
|
||||
.SECONDEXPANSION:
|
||||
$(PLUGIN_TARGETS): $(OUTPUT_BASE)/%.so: $$(wildcard plugins/%/*) $(SOURCES)
|
||||
go build -tags netgo $(RACE) -o $@ -buildmode=plugin plugins/$*/*.go
|
||||
go build -tags netgo $(RACE) -o $@ -buildmode=plugin plugins/$*/plugin/*.go
|
||||
|
||||
.SECONDEXPANSION:
|
||||
$(PLUGIN_DIST): $(OUTPUT_BASE)/%_$(GOOS)_$(GOARCH)$(GOARM).so: $$(wildcard plugins/%/*) $(SOURCES)
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) CC=$(CC) CGO_ENABLED=1 \
|
||||
go build -tags netgo $(EXTRA_TAGS) \
|
||||
-ldflags "-s -w -X main.Version=$(SQUIRCY3_VERSION)" \
|
||||
-o $@ -buildmode=plugin plugins/$*/plugin/*.go
|
||||
|
||||
$(SQUIRCY_TARGET): $(SOURCES)
|
||||
go build -tags netgo $(RACE) -o $@ cmd/squircy/*.go
|
||||
go build -tags netgo $(EXTRA_TAGS) $(RACE) -ldflags "-X main.Version=$(SQUIRCY3_VERSION)-dev" \
|
||||
-o $@ cmd/squircy/main.go cmd/squircy/repl.go $(LINKED_PLUGINS_FILE)
|
||||
|
||||
$(OUTPUT_BASE)/.generated: $(GENERATOR_SOURCES)
|
||||
$(SQUIRCY_DIST): $(OUTPUT_BASE) $(SOURCES)
|
||||
cd cmd/squircy/defconf && \
|
||||
yarn install
|
||||
cd cmd/squircy && \
|
||||
packr2
|
||||
touch $@
|
||||
$(PACKR)
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) CC=$(CC) CGO_ENABLED=1 \
|
||||
go build -tags netgo $(EXTRA_TAGS) \
|
||||
-ldflags "-s -w -X main.Version=$(SQUIRCY3_VERSION)" \
|
||||
-o $@ cmd/squircy/main.go cmd/squircy/repl.go $(LINKED_PLUGINS_FILE)
|
||||
upx $@
|
||||
|
||||
$(OUTPUT_BASE):
|
||||
mkdir -p $(OUTPUT_BASE)
|
||||
|
||||
$(SOURCES): $(OUTPUT_BASE)
|
||||
|
||||
$(GENERATOR_SOURCES): $(OUTPUT_BASE)
|
||||
|
|
19
README.md
19
README.md
|
@ -36,18 +36,18 @@ directory as the root.
|
|||
does not contain config.toml or package.json, defaults will be created.
|
||||
|
||||
```bash
|
||||
out/squircy -interactive -root out
|
||||
out/squircy -interactive -root out/config
|
||||
```
|
||||
|
||||
This will automatically create `config.toml` and `package.json` within the
|
||||
`out/` directory. The default configuration enables all plugins available
|
||||
and connects to freenode. As such, on the first run, expect to see some
|
||||
`out/config/` directory. The default configuration enables all plugins available
|
||||
and connects to libera.chat. As such, on the first run, expect to see some
|
||||
warnings.
|
||||
|
||||
> Expect to see some warnings on the first run. All plugins are enabled, by
|
||||
default, so NodeJS dependencies must be installed for everything to function.
|
||||
|
||||
Modify the default configuration in `out/config.toml` as necessary. Comment
|
||||
Modify the default configuration in `out/config/config.toml` as necessary. Comment
|
||||
out or remove the plugins listed under `extra_plugins` to disable any
|
||||
unwanted plugins.
|
||||
|
||||
|
@ -125,6 +125,17 @@ as shared libraries and loaded at runtime using the Go plugin API.
|
|||
[squircy2](https://squircy.com).
|
||||
- `script` loads javascript files from a configured folder at app startup.
|
||||
|
||||
#### Linking extra plugins at compile-time
|
||||
|
||||
squircy3 supports building-in the extra plugins at compile-time so that they
|
||||
are included in the main binary rather than as separate shared object files.
|
||||
|
||||
Pass `PLUGIN_TYPE=linked` to make to enable this functionality.
|
||||
|
||||
```bash
|
||||
make all PLUGIN_TYPE=linked
|
||||
```
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PluginOptsFlag(fs *flag.FlagSet, name, usage string) {
|
||||
val := make(pluginOptsFlag)
|
||||
PluginOptsFlagVar(fs, &val, name, usage)
|
||||
}
|
||||
|
||||
func PluginOptsFlagVar(fs *flag.FlagSet, val flag.Value, name, usage string) {
|
||||
fs.Var(val, name, usage)
|
||||
}
|
||||
|
||||
type pluginOptsFlag map[string]interface{}
|
||||
|
||||
func (s pluginOptsFlag) String() string {
|
||||
var res []string
|
||||
for k, v := range s {
|
||||
res = append(res, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
return strings.Join(res, " ")
|
||||
}
|
||||
|
||||
func (s pluginOptsFlag) Set(str string) error {
|
||||
p := strings.SplitN(str, "=", 2)
|
||||
if len(p) == 1 {
|
||||
p = append(p, "true")
|
||||
}
|
||||
var v interface{}
|
||||
if p[1] == "true" {
|
||||
v = true
|
||||
} else if p[1] == "false" {
|
||||
v = false
|
||||
} else {
|
||||
v = p[1]
|
||||
}
|
||||
s[p[0]] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s pluginOptsFlag) Get() interface{} {
|
||||
return map[string]interface{}(s)
|
||||
}
|
||||
|
||||
type stringsFlag []string
|
||||
|
||||
func (s stringsFlag) String() string {
|
||||
return strings.Join(s, "")
|
||||
}
|
||||
|
||||
func (s *stringsFlag) Set(str string) error {
|
||||
*s = append(*s, str)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s stringsFlag) Get() interface{} {
|
||||
return []string(s)
|
||||
}
|
||||
|
||||
func DefaultFlags(fs *flag.FlagSet) {
|
||||
CoreFlags(fs, "~/.squircy")
|
||||
IRCFlags(fs)
|
||||
VMFlags(fs)
|
||||
}
|
||||
|
||||
func CoreFlags(fs *flag.FlagSet, root string) {
|
||||
extraPlugins := stringsFlag{}
|
||||
fs.String("root", root, "path to folder containing application data")
|
||||
fs.String("log-level", "info", "controls verbosity of logging output")
|
||||
fs.Var(&extraPlugins, "plugin", "path to shared plugin .so file, multiple plugins may be given")
|
||||
PluginOptsFlag(fs, "plugin-option", "specify extra plugin configuration option, format: key=value")
|
||||
}
|
||||
|
||||
func IRCFlags(fs *flag.FlagSet) {
|
||||
fs.Bool("irc-auto", false, "automatically connect to irc")
|
||||
fs.String("irc-nick", "squishyjones", "specify irc nickname")
|
||||
fs.String("irc-user", "mrjones", "specify irc user")
|
||||
fs.String("irc-network", "chat.freenode.net:6697", "specify irc network")
|
||||
fs.Bool("irc-tls", true, "use tls encryption when connecting to irc")
|
||||
fs.Bool("irc-sasl", false, "use sasl authentication")
|
||||
fs.String("irc-sasl-username", "", "specify sasl username")
|
||||
fs.String("irc-sasl-password", "", "specify sasl password")
|
||||
fs.String("irc-server-password", "", "specify server password")
|
||||
}
|
||||
|
||||
func VMFlags(fs *flag.FlagSet) {
|
||||
fs.String("vm-modules-path", "node_modules", "specify javascript modules path")
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
// import "code.dopame.me/veonik/squircy3/cli"
|
||||
package cli
|
||||
// Package cli makes reusable the core parts of the squircy command.
|
||||
package cli // import "code.dopame.me/veonik/squircy3/cli"
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
@ -8,9 +8,9 @@ import (
|
|||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
tilde "gopkg.in/mattes/go-expand-tilde.v1"
|
||||
|
||||
"code.dopame.me/veonik/squircy3/config"
|
||||
"code.dopame.me/veonik/squircy3/event"
|
||||
|
@ -20,13 +20,13 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
RootDir string `toml:"root_path",flag:"root"`
|
||||
RootDir string `toml:"root_path" flag:"root"`
|
||||
PluginDir string `toml:"plugin_path"`
|
||||
ExtraPlugins []string `toml:"extra_plugins"`
|
||||
ExtraPlugins []string `toml:"extra_plugins" flag:"plugin"`
|
||||
|
||||
PluginOptions map[string]interface{}
|
||||
PluginOptions map[string]interface{} `flag:"plugin_option"`
|
||||
|
||||
LogLevel string `toml:"log_level"`
|
||||
LogLevel logrus.Level `toml:"log_level"`
|
||||
|
||||
// Specify additional plugins that are a part of the main executable.
|
||||
LinkedPlugins []plugin.Initializer
|
||||
|
@ -40,7 +40,7 @@ type Manager struct {
|
|||
stop chan os.Signal
|
||||
}
|
||||
|
||||
func NewManager(rootDir string, pluginOptions map[string]interface{}, extraPlugins ...string) (*Manager, error) {
|
||||
func NewManager() (*Manager, error) {
|
||||
m := plugin.NewManager()
|
||||
// initialize only the config plugin so that it can be configured before
|
||||
// other plugins are initialized
|
||||
|
@ -48,22 +48,47 @@ func NewManager(rootDir string, pluginOptions map[string]interface{}, extraPlugi
|
|||
if err := configure(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf := Config{
|
||||
RootDir: rootDir,
|
||||
PluginDir: filepath.Join(rootDir, "plugins"),
|
||||
ExtraPlugins: extraPlugins,
|
||||
PluginOptions: pluginOptions,
|
||||
}
|
||||
conf := Config{}
|
||||
// configure the config plugin!
|
||||
cf := filepath.Join(rootDir, "config.toml")
|
||||
err := config.ConfigurePlugin(m,
|
||||
config.WithRequiredOptions("root_path", "log_level"),
|
||||
config.WithFilteredOption("root_path", func(s string, val config.Value) (config.Value, error) {
|
||||
vs, ok := val.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("expected root_path to be string but got %T", vs)
|
||||
}
|
||||
vs, err := tilde.Expand(vs)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to expand root directory: %s", err)
|
||||
}
|
||||
return vs, nil
|
||||
}),
|
||||
config.WithFilteredOption("log_level", func(s string, val config.Value) (config.Value, error) {
|
||||
if v, ok := val.(logrus.Level); ok {
|
||||
return v, nil
|
||||
}
|
||||
vs, ok := val.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("expected log_level to be string but got %T", vs)
|
||||
}
|
||||
lvl, err := logrus.ParseLevel(vs)
|
||||
if err != nil {
|
||||
lvl = logrus.InfoLevel
|
||||
logrus.Warnf("config: defaulting to info log level: failed to parse %s as log level: %s", lvl, err)
|
||||
}
|
||||
return lvl, nil
|
||||
}),
|
||||
config.WithInitValue(&conf),
|
||||
config.WithValuesFromTOMLFile(cf),
|
||||
config.WithValuesFromFlagSet(flag.CommandLine),
|
||||
config.WithValuesFromMap(conf.PluginOptions))
|
||||
config.WithValuesFromMap(&conf.PluginOptions))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cf := filepath.Join(conf.RootDir, "config.toml")
|
||||
// Now that we have determined the root directory
|
||||
if err := config.ConfigurePlugin(m, config.WithValuesFromTOMLFile(cf)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Manager{
|
||||
plugins: m,
|
||||
stop: make(chan os.Signal, 10),
|
||||
|
@ -97,6 +122,7 @@ func (manager *Manager) Start() error {
|
|||
|
||||
// load remaining extra plugins
|
||||
for _, pl := range manager.ExtraPlugins {
|
||||
logrus.Tracef("core: loading extra plugin: %s", pl)
|
||||
if !filepath.IsAbs(pl) {
|
||||
pl = filepath.Join(manager.PluginDir, pl)
|
||||
}
|
||||
|
@ -162,7 +188,7 @@ func (manager *Manager) Loop() error {
|
|||
case syscall.SIGTERM:
|
||||
manager.Stop()
|
||||
default:
|
||||
logrus.Debugln("Received signal", s, "but not doing anything with it")
|
||||
logrus.Debugln("core: received signal", s, "but not doing anything with it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
// +build linked_plugins
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/plugin"
|
||||
|
||||
babel "code.dopame.me/veonik/squircy3/plugins/babel"
|
||||
node_compat2 "code.dopame.me/veonik/squircy3/plugins/node_compat"
|
||||
script "code.dopame.me/veonik/squircy3/plugins/script"
|
||||
squircy2_compat "code.dopame.me/veonik/squircy3/plugins/squircy2_compat"
|
||||
)
|
||||
|
||||
var linkedPlugins = []plugin.Initializer{
|
||||
plugin.InitializerFunc(babel.Initialize),
|
||||
plugin.InitializerFunc(node_compat2.Initialize),
|
||||
plugin.InitializerFunc(script.Initialize),
|
||||
plugin.InitializerFunc(squircy2_compat.Initialize)}
|
|
@ -6,82 +6,35 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
tilde "gopkg.in/mattes/go-expand-tilde.v1"
|
||||
|
||||
"code.dopame.me/veonik/squircy3/cli"
|
||||
"code.dopame.me/veonik/squircy3/irc"
|
||||
)
|
||||
|
||||
type stringsFlag []string
|
||||
var Version = "SNAPSHOT"
|
||||
|
||||
func (s stringsFlag) String() string {
|
||||
return strings.Join(s, "")
|
||||
}
|
||||
func (s *stringsFlag) Set(str string) error {
|
||||
*s = append(*s, str)
|
||||
return nil
|
||||
}
|
||||
|
||||
type pluginOptsFlag map[string]interface{}
|
||||
|
||||
func (s pluginOptsFlag) String() string {
|
||||
var res []string
|
||||
for k, v := range s {
|
||||
res = append(res, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(res, " ")
|
||||
}
|
||||
|
||||
func (s pluginOptsFlag) Set(str string) error {
|
||||
p := strings.SplitN(str, "=", 2)
|
||||
if len(p) == 1 {
|
||||
p = append(p, "true")
|
||||
}
|
||||
var v interface{}
|
||||
if p[1] == "true" {
|
||||
v = true
|
||||
} else if p[1] == "false" {
|
||||
v = false
|
||||
} else {
|
||||
v = p[1]
|
||||
}
|
||||
s[p[0]] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
type stringLevel logrus.Level
|
||||
|
||||
func (s stringLevel) String() string {
|
||||
return logrus.Level(s).String()
|
||||
}
|
||||
func (s *stringLevel) Set(str string) error {
|
||||
l, err := logrus.ParseLevel(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = stringLevel(l)
|
||||
return nil
|
||||
}
|
||||
|
||||
var rootDir string
|
||||
var extraPlugins stringsFlag
|
||||
var pluginOptions = make(pluginOptsFlag)
|
||||
var logLevel = stringLevel(logrus.InfoLevel)
|
||||
var interactive bool
|
||||
|
||||
func unboxAll(rootDir string) error {
|
||||
box := packr.New("defconf", "./defconf")
|
||||
if _, err := os.Stat(rootDir); err == nil {
|
||||
// root directory already exists, don't muck with it
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to create root directory")
|
||||
}
|
||||
box := packr.New("defconf", "./defconf")
|
||||
for _, f := range box.List() {
|
||||
dst := filepath.Join(rootDir, f)
|
||||
if _, err := os.Stat(dst); os.IsNotExist(err) {
|
||||
logrus.Infof("Creating default %s as %s", f, dst)
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to recreate directory")
|
||||
}
|
||||
logrus.Infof("Creating default %s", dst)
|
||||
d, err := box.Find(f)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get contents of boxed %s", f)
|
||||
|
@ -95,21 +48,10 @@ func unboxAll(rootDir string) error {
|
|||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&rootDir, "root", "~/.squircy", "path to folder containing squircy data")
|
||||
flag.Var(&logLevel, "log-level", "controls verbosity of logging output")
|
||||
flag.Var(&extraPlugins, "plugin", "path to shared plugin .so file, multiple plugins may be given")
|
||||
printVersion := false
|
||||
flag.BoolVar(&interactive, "interactive", false, "start interactive-read-evaluate-print (REPL) session")
|
||||
flag.Var(&pluginOptions, "plugin-option", "specify extra plugin configuration option, format: key=value")
|
||||
flag.Bool("irc-auto", false, "automatically connect to irc")
|
||||
flag.String("irc-nick", "squishyjones", "specify irc nickname")
|
||||
flag.String("irc-user", "mrjones", "specify irc user")
|
||||
flag.String("irc-network", "chat.freenode.net:6697", "specify irc network")
|
||||
flag.Bool("irc-tls", true, "use tls encryption when connecting to irc")
|
||||
flag.Bool("irc-sasl", false, "use sasl authentication")
|
||||
flag.String("irc-sasl-username", "", "specify sasl username")
|
||||
flag.String("irc-sasl-password", "", "specify sasl password")
|
||||
flag.String("irc-server-password", "", "specify server password")
|
||||
flag.String("vm-modules-path", "node_modules", "specify javascript modules path")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information")
|
||||
cli.DefaultFlags(flag.CommandLine)
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: ", os.Args[0], "[options]")
|
||||
|
@ -120,28 +62,34 @@ func init() {
|
|||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
var err error
|
||||
if rootDir, err = tilde.Expand(rootDir); err != nil {
|
||||
logrus.Fatalln("core: failed determine root directory:", err)
|
||||
}
|
||||
if err := unboxAll(rootDir); err != nil {
|
||||
logrus.Fatalln("core: failed to unbox defaults:", err)
|
||||
|
||||
if printVersion {
|
||||
fmt.Printf("squircy3 %s\n", Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
logrus.SetLevel(logrus.Level(logLevel))
|
||||
m, err := cli.NewManager(rootDir, pluginOptions, extraPlugins...)
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
m, err := cli.NewManager()
|
||||
if err != nil {
|
||||
logrus.Fatalln("core: error initializing squircy:", err)
|
||||
}
|
||||
m.LinkedPlugins = append(m.LinkedPlugins, linkedPlugins...)
|
||||
logrus.SetLevel(m.LogLevel)
|
||||
if err := unboxAll(m.RootDir); err != nil {
|
||||
logrus.Fatalln("core: failed to unbox defaults:", err)
|
||||
}
|
||||
if err := m.Start(); err != nil {
|
||||
logrus.Fatalln("core: error starting squircy:", err)
|
||||
}
|
||||
ircm, err := irc.FromPlugins(m.Plugins())
|
||||
if err != nil {
|
||||
logrus.Errorln("core: failed to set irc version string:", err)
|
||||
}
|
||||
ircm.SetVersionString(fmt.Sprintf("squircy3 %s", Version))
|
||||
if interactive {
|
||||
go func() {
|
||||
Repl(m)
|
||||
}()
|
||||
go Repl(m)
|
||||
}
|
||||
if err = m.Loop(); err != nil {
|
||||
logrus.Fatalln("core: exiting main loop with error:", err)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// +build !linked_plugins
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/plugin"
|
||||
)
|
||||
|
||||
var linkedPlugins []plugin.Initializer
|
|
@ -1,8 +1,18 @@
|
|||
plugin_path="/squircy/plugins"
|
||||
extra_plugins=[
|
||||
# babel seamlessly transpiles javascript before executing it in squircy3, enabling the use of
|
||||
# ES2017+ features in your scripts.
|
||||
"babel.so",
|
||||
|
||||
# node_compat is a barebones compatibility layer for NodeJS APIs. It supports a limited set of
|
||||
# features such as parts of child_process, net, and crypto. Requires babel.
|
||||
"node_compat.so",
|
||||
|
||||
# squircy2_compat is a compatibility layer for legacy squIRCy2 scripts.
|
||||
"squircy2_compat.so",
|
||||
|
||||
# script is a plugin that loads scripts from a directory and executes them during application
|
||||
# startup.
|
||||
"script.so",
|
||||
]
|
||||
|
||||
|
@ -26,12 +36,15 @@ enable=true
|
|||
scripts_path="/home/squircy/.squircy/scripts"
|
||||
|
||||
[squircy2_compat]
|
||||
# set enable_file_api to true to allow scripts to read from the filesystem.
|
||||
enable_file_api=false
|
||||
# set the root directory of the filesystem access; files outside this root will not be loaded.
|
||||
file_api_root="/home/squircy/.squircy/files"
|
||||
#owner_nick=""
|
||||
#owner_host=""
|
||||
data_path="/home/squircy/.squircy/data"
|
||||
|
||||
[node_compat]
|
||||
# set enable_exec to true to allow scripts to spawn child processes.
|
||||
enable_exec=false
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ func WithSection(sec Section, options ...SetupOption) SetupOption {
|
|||
return err
|
||||
}
|
||||
s.sections[ns.name] = ns
|
||||
s.sectionsOrdered = append(s.sectionsOrdered, ns)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -106,12 +107,47 @@ func WithOption(name string) SetupOption {
|
|||
func WithOptions(names ...string) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
for _, n := range names {
|
||||
s.options[n] = false
|
||||
if _, ok := s.options[n]; !ok {
|
||||
s.options[n] = nil
|
||||
s.optionsOrdered = append(s.optionsOrdered, n)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithValidatedOption adds a value validator for the given option.
|
||||
// Validator functions accept the name of the option and its value, and
|
||||
// return an error if the value is not considered valid.
|
||||
func WithValidatedOption(name string, fn func(string, Value) error) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
s.options[name] = append(s.options[name], fn)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilteredOption adds a filter that may modify the option's value.
|
||||
// Filters are applied after
|
||||
func WithFilteredOption(name string, fn func(string, Value) (Value, error)) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
s.filters[name] = append(s.filters[name], fn)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateRequired is a validator that ensures an option is not blank.
|
||||
// Any nil value or string with length of zero is considered blank.
|
||||
func ValidateRequired(o string, v Value) error {
|
||||
var nilValue Value
|
||||
if v == nil || v == nilValue {
|
||||
return errors.Errorf(`required option "%s" is empty`, o)
|
||||
}
|
||||
if vs, ok := v.(string); ok && len(vs) == 0 {
|
||||
return errors.Errorf(`required option "%s" is empty`, o)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithRequiredOption adds a required option to the Config.
|
||||
func WithRequiredOption(name string) SetupOption {
|
||||
return WithRequiredOptions(name)
|
||||
|
@ -121,7 +157,9 @@ func WithRequiredOption(name string) SetupOption {
|
|||
func WithRequiredOptions(names ...string) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
for _, n := range names {
|
||||
s.options[n] = true
|
||||
if err := WithValidatedOption(n, ValidateRequired)(s); err != nil {
|
||||
return errors.Wrapf(err, "validation failed for %s", n)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -134,7 +172,7 @@ func WithInheritedOption(name string) SetupOption {
|
|||
if ps == nil {
|
||||
return errors.Errorf("config: unable to inherit option '%s' for section %s; no parent found", name, s.name)
|
||||
}
|
||||
s.inherits[name] = struct{}{}
|
||||
s.inherits = append(s.inherits, name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"code.dopame.me/veonik/squircy3/config"
|
||||
)
|
||||
|
||||
|
@ -18,7 +20,7 @@ func TestWithValuesFromFlagSet(t *testing.T) {
|
|||
co := &Config{"veonik", &struct{ Age int }{30}}
|
||||
fs := flag.NewFlagSet("", flag.ExitOnError)
|
||||
fs.String("name", "", "your name")
|
||||
fs.Int("bio-age", 0, "you age")
|
||||
fs.Int("bio-age", 0, "your age")
|
||||
if err := fs.Parse([]string{"-name", "tyler", "-bio-age", "31"}); err != nil {
|
||||
t.Errorf("unexpected error parsing flagset: %s", err)
|
||||
return
|
||||
|
@ -58,3 +60,55 @@ func TestWithValuesFromFlagSet(t *testing.T) {
|
|||
// Hi, tyler!
|
||||
// tyler is 31.
|
||||
}
|
||||
|
||||
func TestWithValuesFromFlagSetDefaultValues(t *testing.T) {
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
co := map[string]interface{}{}
|
||||
fs := flag.NewFlagSet("", flag.ExitOnError)
|
||||
fs.String("name", "veonik", "your name")
|
||||
fs.Int("bio-age", 31, "your age")
|
||||
if err := fs.Parse([]string{}); err != nil {
|
||||
t.Errorf("unexpected error parsing flagset: %s", err)
|
||||
return
|
||||
}
|
||||
c, err := config.Wrap(co,
|
||||
config.WithRequiredOption("name"),
|
||||
config.WithGenericSection("bio", config.WithRequiredOption("age")),
|
||||
config.WithValuesFromFlagSet(fs))
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Hi, %s!\n", co["name"])
|
||||
n, ok := c.String("name")
|
||||
if !ok {
|
||||
t.Errorf("expected name to be a string")
|
||||
return
|
||||
}
|
||||
b, err := c.Section("bio")
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
}
|
||||
a, ok := b.Int("age")
|
||||
if !ok {
|
||||
t.Errorf("expected age to be an int")
|
||||
return
|
||||
}
|
||||
if co["name"] != n {
|
||||
t.Errorf("expected Name option (%s) to match Name field on Config struct (%s)", n, co["name"])
|
||||
return
|
||||
}
|
||||
bio, ok := co["bio"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Errorf("expected map to contain another map with key bio, but got: %v", bio)
|
||||
return
|
||||
}
|
||||
if bio["age"] != a {
|
||||
t.Errorf("expected bio.age option (%d) to match Age field on Bio struct (%d)", a, bio["age"])
|
||||
}
|
||||
fmt.Printf("%s is %d.\n", n, a)
|
||||
// Outputs:
|
||||
// Hi, tyler!
|
||||
// tyler is 31.
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ func TestWithValuesFromMap(t *testing.T) {
|
|||
c, err := config.Wrap(co,
|
||||
config.WithRequiredOption("Name"),
|
||||
config.WithGenericSection("Bio", config.WithRequiredOption("Age")),
|
||||
config.WithValuesFromMap(opts))
|
||||
config.WithValuesFromMap(&opts))
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
|
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
|
@ -18,6 +19,10 @@ func populateValuesFromTOMLFile(filename string) postSetupOption {
|
|||
s.raw = make(map[string]interface{})
|
||||
}
|
||||
if _, err := toml.DecodeFile(filename, &s.raw); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logrus.Warnf("config: unable to load toml config, but not aborting: %s", err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -28,7 +33,7 @@ func populateValuesFromTOMLFile(filename string) postSetupOption {
|
|||
// TOML file.
|
||||
func WithValuesFromTOMLFile(filename string) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
return s.addPostSetup(populateValuesFromTOMLFile(filename))
|
||||
return s.prependPostSetup(populateValuesFromTOMLFile(filename))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,10 +50,10 @@ func newNameFieldMapper(s *Setup) *nameFieldMapper {
|
|||
return &nameFieldMapper{s}
|
||||
}
|
||||
|
||||
// normalize converts the given name into a normal, underscorized name.
|
||||
// normalizeName converts the given name into a normal, underscorized name.
|
||||
// Dashes are converted to underscores, camel case is separated by underscores
|
||||
// and converts everything to lower-case.
|
||||
func (fm *nameFieldMapper) normalize(name string) string {
|
||||
func normalizeName(name string) string {
|
||||
name = dashAndSpaceMatcher.ReplaceAllString(name, "_")
|
||||
var a []string
|
||||
for _, sub := range camelCaseMatcher.FindAllStringSubmatch(name, -1) {
|
||||
|
@ -69,62 +74,63 @@ func (fm *nameFieldMapper) normalize(name string) string {
|
|||
// is converted into the path:
|
||||
// ["Templating","Twig","views_path"]
|
||||
func (fm *nameFieldMapper) Map(flagName string) (path []string) {
|
||||
normal := fm.normalize(flagName)
|
||||
normal := normalizeName(flagName)
|
||||
s := fm.s
|
||||
loop:
|
||||
for s != nil {
|
||||
// a valueInspector here handles struct tag aliases.
|
||||
is, err := inspect(s.initial)
|
||||
if err != nil {
|
||||
logrus.Debugf("config: unable to create valueInspector for section '%s': %s", s.name, err)
|
||||
} else {
|
||||
if _, err := is.Get(normal); err == nil {
|
||||
path = append(path, normal)
|
||||
logrus.Debugf("config: valueInspector found match for field '%s' in section '%s'", normal, s.name)
|
||||
s = nil
|
||||
goto loop
|
||||
} else {
|
||||
logrus.Debugf("config: valueInspector returned error for '%s' in section '%s': %s", normal, s.name, err)
|
||||
}
|
||||
}
|
||||
// check for a match in options next
|
||||
for k := range s.options {
|
||||
kn := fm.normalize(k)
|
||||
if kn == normal {
|
||||
// found it
|
||||
path = append(path, k)
|
||||
logrus.Debugf("config: found option with name '%s' in section '%s'", normal, s.name)
|
||||
s = nil
|
||||
goto loop
|
||||
}
|
||||
}
|
||||
// check for a matching section, using the name as a prefix
|
||||
for k, ks := range s.sections {
|
||||
kn := fm.normalize(k) + "_"
|
||||
kn := normalizeName(k) + "_"
|
||||
if strings.HasPrefix(normal, kn) {
|
||||
// found the next step in the path
|
||||
normal = strings.Replace(normal, kn, "", 1)
|
||||
path = append(path, k)
|
||||
logrus.Debugf("config: descending into section %s (from %s) to find match for option '%s'", ks.name, s.name, normal)
|
||||
logrus.Tracef("config: descending into section %s (from %s) to find match for option '%s'", ks.name, s.name, normal)
|
||||
s = ks
|
||||
goto loop
|
||||
}
|
||||
}
|
||||
// check for a match in options next
|
||||
for k := range s.options {
|
||||
kn := normalizeName(k)
|
||||
if kn == normal {
|
||||
// found it
|
||||
path = append(path, k)
|
||||
logrus.Tracef("config: found option with name '%s' in section '%s'", normal, s.name)
|
||||
s = nil
|
||||
goto loop
|
||||
}
|
||||
}
|
||||
// finally, try a valueInspector here to handle struct tag aliases.
|
||||
is, err := inspect(s.initial)
|
||||
if err != nil {
|
||||
logrus.Debugf("config: unable to create valueInspector for section '%s': %s", s.name, err)
|
||||
} else {
|
||||
if name, err := is.Normalize(normal); err == nil {
|
||||
path = append(path, name)
|
||||
logrus.Tracef("config: valueInspector found match for field '%s' in section '%s'", name, s.name)
|
||||
s = nil
|
||||
goto loop
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), "no field with name") {
|
||||
logrus.Debugf("config: valueInspector returned error for '%s' in section '%s': %s", normal, s.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func visitNamedOption(s *Setup, f string, fv interface{}, m *nameFieldMapper) {
|
||||
func visitNamedOption(name string, raw map[string]interface{}, f string, fv interface{}, m *nameFieldMapper, override bool) {
|
||||
path := m.Map(f)
|
||||
if len(path) == 0 {
|
||||
logrus.Debugf("config: did not match anything for named option '%s' for section %s", f, s.name)
|
||||
logrus.Debugf("config: did not match anything for named option '%s' for section %s", f, name)
|
||||
return
|
||||
}
|
||||
logrus.Debugf("config: named option '%s' mapped to path: %v", f, path)
|
||||
logrus.Debugf("config: named option '%s' setting to: %T(%v)", f, fv, fv)
|
||||
logrus.Debugf("config: named option '%s' mapped to path %v and value %T(%v)", f, path, fv, fv)
|
||||
val := fv
|
||||
v := s.raw
|
||||
v := raw
|
||||
i := 0
|
||||
// iterate over all but the last part of the path, descending into a
|
||||
// new section with each iteration.
|
||||
|
@ -133,8 +139,13 @@ func visitNamedOption(s *Setup, f string, fv interface{}, m *nameFieldMapper) {
|
|||
v = vs
|
||||
} else {
|
||||
if vr, ok := v[path[i]]; ok {
|
||||
if vrn, ok := vr.(map[string]interface{}); ok {
|
||||
// value exists and is already a map[string]interface{}, use it.
|
||||
v = vrn
|
||||
continue
|
||||
}
|
||||
// there is no path[0-1] so figure out the name accordingly
|
||||
secn := s.name
|
||||
secn := name
|
||||
if i > 0 {
|
||||
secn = path[i-1]
|
||||
}
|
||||
|
@ -145,25 +156,26 @@ func visitNamedOption(s *Setup, f string, fv interface{}, m *nameFieldMapper) {
|
|||
v = nv
|
||||
}
|
||||
}
|
||||
if !override {
|
||||
if _, ok := v[path[i]]; ok {
|
||||
// value is already set, don't override it.
|
||||
return
|
||||
}
|
||||
}
|
||||
// use the last element in the path to set the right option.
|
||||
v[path[i]] = val
|
||||
}
|
||||
|
||||
func populateValuesFromFlagSet(fs *flag.FlagSet) postSetupOption {
|
||||
return func(s *Setup) error {
|
||||
if !fs.Parsed() {
|
||||
return errors.Errorf("given FlagSet must be parsed")
|
||||
}
|
||||
if s.raw == nil {
|
||||
s.raw = make(map[string]interface{})
|
||||
}
|
||||
logrus.Tracef("config: populating values from FlagSet %s", fs.Name())
|
||||
m := newNameFieldMapper(s)
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
var v interface{} = f.Value.String()
|
||||
if fv, ok := f.Value.(flag.Getter); ok {
|
||||
v = fv.Get()
|
||||
}
|
||||
visitNamedOption(s, f.Name, v, m)
|
||||
visitNamedOption(s.name, s.raw, f.Name, v, m, true)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
@ -175,26 +187,39 @@ func WithValuesFromFlagSet(fs *flag.FlagSet) SetupOption {
|
|||
if !fs.Parsed() {
|
||||
return errors.Errorf("given FlagSet must be parsed")
|
||||
}
|
||||
return s.addPostSetup(populateValuesFromFlagSet(fs))
|
||||
}
|
||||
}
|
||||
|
||||
func populateValuesFromMap(vs map[string]interface{}) postSetupOption {
|
||||
return func(s *Setup) error {
|
||||
if s.raw == nil {
|
||||
s.raw = make(map[string]interface{})
|
||||
}
|
||||
m := newNameFieldMapper(s)
|
||||
for f, fv := range vs {
|
||||
visitNamedOption(s, f, fv, m)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
var v interface{} = f.Value.String()
|
||||
if fv, ok := f.Value.(flag.Getter); ok {
|
||||
v = fv.Get()
|
||||
}
|
||||
visitNamedOption(s.name, s.raw, f.Name, v, m, false)
|
||||
})
|
||||
|
||||
return s.appendPostSetup(populateValuesFromFlagSet(fs))
|
||||
}
|
||||
}
|
||||
|
||||
func populateValuesFromMap(vs *map[string]interface{}) postSetupOption {
|
||||
return func(s *Setup) error {
|
||||
logrus.Tracef("config: populating values from map: %p", vs)
|
||||
if s.raw == nil {
|
||||
s.raw = make(map[string]interface{})
|
||||
}
|
||||
m := newNameFieldMapper(s)
|
||||
for f, fv := range *vs {
|
||||
visitNamedOption(s.name, s.raw, f, fv, m, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithValuesFromMap populates the Config using the given map.
|
||||
func WithValuesFromMap(vs map[string]interface{}) SetupOption {
|
||||
func WithValuesFromMap(vs *map[string]interface{}) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
return s.addPostSetup(populateValuesFromMap(vs))
|
||||
return s.appendPostSetup(populateValuesFromMap(vs))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ func pluginFromPlugins(m *plugin.Manager) (*configPlugin, error) {
|
|||
return mp, nil
|
||||
}
|
||||
|
||||
// ConfigurePlugin applies the given options to the registered config plugin.
|
||||
func ConfigurePlugin(m *plugin.Manager, opts ...SetupOption) error {
|
||||
mp, err := pluginFromPlugins(m)
|
||||
if err != nil {
|
||||
|
@ -26,6 +27,7 @@ func ConfigurePlugin(m *plugin.Manager, opts ...SetupOption) error {
|
|||
return mp.Configure(opts...)
|
||||
}
|
||||
|
||||
// Initialize is a plugin.Initializer that initializes a config plugin.
|
||||
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
|
||||
p := &configPlugin{}
|
||||
return p, nil
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -19,6 +17,12 @@ type postSetupOption func(c *Setup) error
|
|||
// A protoFunc is a function that returns the initial value for a config.
|
||||
type protoFunc func() Value
|
||||
|
||||
// An optionFilter modifies the received value and returns the result.
|
||||
type optionFilter func(name string, val Value) (Value, error)
|
||||
|
||||
// An optionValidator returns an error if the given value is invalid.
|
||||
type optionValidator func(name string, val Value) error
|
||||
|
||||
// Setup is a container struct with information on how to setup a given Config.
|
||||
type Setup struct {
|
||||
name string
|
||||
|
@ -31,9 +35,12 @@ type Setup struct {
|
|||
|
||||
raw map[string]interface{}
|
||||
|
||||
sections map[string]*Setup
|
||||
options map[string]bool
|
||||
inherits map[string]struct{}
|
||||
sectionsOrdered []*Setup
|
||||
sections map[string]*Setup
|
||||
optionsOrdered []string
|
||||
options map[string][]optionValidator
|
||||
filters map[string][]optionFilter
|
||||
inherits []string
|
||||
|
||||
post []postSetupOption
|
||||
}
|
||||
|
@ -48,17 +55,23 @@ func newSetup(name string, parent *Setup) *Setup {
|
|||
parent: parent,
|
||||
raw: make(map[string]interface{}),
|
||||
sections: make(map[string]*Setup),
|
||||
options: make(map[string]bool),
|
||||
inherits: make(map[string]struct{}),
|
||||
options: make(map[string][]optionValidator),
|
||||
filters: make(map[string][]optionFilter),
|
||||
}
|
||||
}
|
||||
|
||||
// addPostSetup adds one or more postSetupOptions.
|
||||
func (s *Setup) addPostSetup(options ...postSetupOption) error {
|
||||
// appendPostSetup adds one or more postSetupOptions to the end.
|
||||
func (s *Setup) appendPostSetup(options ...postSetupOption) error {
|
||||
s.post = append(s.post, options...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// prependPostSetup adds one or more postSetupOptions to the beginning.
|
||||
func (s *Setup) prependPostSetup(options ...postSetupOption) error {
|
||||
s.post = append(append([]postSetupOption{}, options...), s.post...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply calls each SetupOption, halting on the first error encountered.
|
||||
func (s *Setup) apply(options ...SetupOption) error {
|
||||
// clear post options, they will be re-added by the regular options.
|
||||
|
@ -83,15 +96,11 @@ func (s *Setup) validate() error {
|
|||
if s.config == nil {
|
||||
return errors.New(`expected config to be populated, found nil`)
|
||||
}
|
||||
for o, reqd := range s.options {
|
||||
if reqd {
|
||||
var nilValue Value
|
||||
v, ok := s.config.Get(o)
|
||||
if !ok || v == nil || v == nilValue {
|
||||
return errors.Errorf(`required option "%s" is empty`, o)
|
||||
}
|
||||
if vs, ok := v.(string); ok && len(vs) == 0 {
|
||||
return errors.Errorf(`required option "%s" is empty`, o)
|
||||
for o, ovs := range s.options {
|
||||
for _, validator := range ovs {
|
||||
v, _ := s.config.Get(o)
|
||||
if err := validator(o, v); err != nil {
|
||||
return errors.Wrapf(err, `failed to validate option "%s"`, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +116,7 @@ func (s *Setup) validate() error {
|
|||
func walkAndWrap(s *Setup) error {
|
||||
wrapErr := func(err error) error {
|
||||
if s.name != "root" {
|
||||
return errors.WithMessage(err, fmt.Sprintf("section %s", s.name))
|
||||
return errors.WithMessage(err, "section "+s.name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -142,7 +151,7 @@ func walkAndWrap(s *Setup) error {
|
|||
// walkInherits synchronizes inherited options and sections between this and
|
||||
// the parent.
|
||||
func walkInherits(s *Setup) error {
|
||||
for si := range s.inherits {
|
||||
for _, si := range s.inherits {
|
||||
if s.parent == nil {
|
||||
return errors.Errorf("unable to inherit option %s from non-existent parent", si)
|
||||
}
|
||||
|
@ -161,7 +170,7 @@ func walkInherits(s *Setup) error {
|
|||
|
||||
// walkSections walks through each section, populating a Config for each.
|
||||
func walkSections(s *Setup) error {
|
||||
for _, ns := range s.sections {
|
||||
for _, ns := range s.sectionsOrdered {
|
||||
if v, ok := s.raw[ns.name].(map[string]interface{}); ok {
|
||||
ns.raw = v
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
|
@ -19,6 +18,8 @@ type configurable struct {
|
|||
Value
|
||||
options configurableOpts
|
||||
inspector *valueInspector
|
||||
|
||||
filters map[string][]optionFilter
|
||||
}
|
||||
|
||||
func newConfigurable(s *Setup) (*configurable, error) {
|
||||
|
@ -29,11 +30,12 @@ func newConfigurable(s *Setup) (*configurable, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &configurable{Value: s.initial, options: make(configurableOpts), inspector: is}
|
||||
c := &configurable{Value: s.initial, options: make(configurableOpts), filters: s.filters, inspector: is}
|
||||
for k, v := range s.raw {
|
||||
logrus.Tracef("Setting %s.%s to %T(%v)", s.name, k, v, v)
|
||||
c.Set(k, v)
|
||||
}
|
||||
for k := range s.options {
|
||||
for _, k := range s.optionsOrdered {
|
||||
v, err := c.inspector.Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -95,9 +97,16 @@ func (c *configurable) Int(key string) (int, bool) {
|
|||
func (c *configurable) Set(key string, val Value) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println(err)
|
||||
logrus.Errorf("config: unexpected panic while trying to set option %s: %s", key, err)
|
||||
}
|
||||
}()
|
||||
for _, filter := range c.filters[key] {
|
||||
if nv, err := filter(key, val); err != nil {
|
||||
logrus.Errorf("config: error while filtering value for option %s: %s", key, err)
|
||||
} else {
|
||||
val = nv
|
||||
}
|
||||
}
|
||||
c.options[key] = val
|
||||
c.inspector.Set(key, val)
|
||||
}
|
||||
|
@ -147,6 +156,10 @@ func inspect(v Value) (*valueInspector, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (i *valueInspector) Normalize(name string) (string, error) {
|
||||
return i.realName(name)
|
||||
}
|
||||
|
||||
func (i *valueInspector) Get(name string) (Value, error) {
|
||||
return i.valueNamed(name)
|
||||
}
|
||||
|
@ -177,6 +190,11 @@ func (i *valueInspector) Set(name string, val Value) {
|
|||
res = append(res, vs)
|
||||
}
|
||||
}
|
||||
} else if v, ok := val.([]string); ok {
|
||||
res = v
|
||||
} else {
|
||||
logrus.Warnf("config: unsupported value type for slice: %T", val)
|
||||
return
|
||||
}
|
||||
rv = reflect.ValueOf(res)
|
||||
default:
|
||||
|
@ -210,7 +228,7 @@ func (i *valueInspector) valueNamed(name string) (Value, error) {
|
|||
if i.value.Kind() == reflect.Map {
|
||||
m = i.value.MapIndex(reflect.ValueOf(name))
|
||||
if !m.IsValid() {
|
||||
return nil, nil
|
||||
return nil, errors.New("no field with name " + name)
|
||||
}
|
||||
} else {
|
||||
if t, ok := i.tags[name]; ok {
|
||||
|
@ -226,6 +244,27 @@ func (i *valueInspector) valueNamed(name string) (Value, error) {
|
|||
return m.Elem(), nil
|
||||
}
|
||||
|
||||
func (i *valueInspector) realName(name string) (string, error) {
|
||||
var m reflect.Value
|
||||
var res string
|
||||
if i.value.Kind() == reflect.Map {
|
||||
m = i.value.MapIndex(reflect.ValueOf(name))
|
||||
res = name
|
||||
if !m.IsValid() {
|
||||
return res, nil
|
||||
}
|
||||
} else {
|
||||
if t, ok := i.tags[name]; ok {
|
||||
m = i.value.FieldByIndex(t.Index)
|
||||
res = t.CanonName
|
||||
}
|
||||
if !m.IsValid() {
|
||||
return res, errors.New("no field with name " + name)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type structTag struct {
|
||||
Key string
|
||||
Name string
|
||||
|
@ -233,8 +272,9 @@ type structTag struct {
|
|||
}
|
||||
|
||||
type structField struct {
|
||||
Name string
|
||||
Index []int
|
||||
Name string
|
||||
CanonName string
|
||||
Index []int
|
||||
}
|
||||
|
||||
func structTags(v Value) ([]structTag, []structField, error) {
|
||||
|
@ -250,15 +290,22 @@ func structTags(v Value) ([]structTag, []structField, error) {
|
|||
tt := t.Type()
|
||||
for i := 0; i < tt.NumField(); i++ {
|
||||
f := tt.Field(i)
|
||||
ff := structField{f.Name, f.Index}
|
||||
ff := structField{f.Name, "", f.Index}
|
||||
fields = append(fields, ff)
|
||||
tgs, err := structtag.Parse(string(f.Tag))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, tg := range tgs.Tags() {
|
||||
if len(ff.CanonName) == 0 {
|
||||
// try to use the first tag as the canonical name for each field
|
||||
ff.CanonName = tg.Name
|
||||
}
|
||||
tags = append(tags, structTag{Key: tg.Key, Name: tg.Name, Field: ff})
|
||||
}
|
||||
if len(ff.CanonName) == 0 {
|
||||
ff.CanonName = normalizeName(ff.Name)
|
||||
}
|
||||
}
|
||||
return tags, fields, nil
|
||||
}
|
||||
|
|