commit f0abca60dbd4fd70b4cf5f0f8d6626a75705cb49 Author: Artёm Mishin Date: Fri Apr 25 11:20:31 2025 +0300 feat: Initial diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ce8bd5b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/universe.iml b/.idea/universe.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/universe.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..504e49d --- /dev/null +++ b/app/app.go @@ -0,0 +1,89 @@ +package app + +import ( + "go.mrlokop.dev/universe/core/config" + "go.mrlokop.dev/universe/core/errors" + "go.mrlokop.dev/universe/core/modules" + "go.uber.org/fx" + "go.uber.org/fx/fxevent" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "sync/atomic" +) + +var ( + ErrAlreadyStarted = errors.Type("ErrAlreadyStarted", "already started") + ErrUnknownModule = errors.Type("ErrUnknownModule", "specified module does not exist") +) + +type Application interface { + Config() *config.UniverseConfig + Run() error +} + +type UniverseApplication struct { + fx *fx.App + config *config.UniverseConfig + options []fx.Option + started atomic.Bool +} + +func (app *UniverseApplication) Config() *config.UniverseConfig { + return app.config +} + +func (app *UniverseApplication) Run() error { + if !app.started.CompareAndSwap(false, true) { + return ErrAlreadyStarted.CreateEmpty() + } + + options := make([]fx.Option, 0) + options = append(options, app.options...) + options = append(options, + fx.Provide(func() Application { + return app + }), + fx.WithLogger(func(log *zap.Logger) fxevent.Logger { + logger := &fxevent.ZapLogger{Logger: log.WithOptions(zap.AddCallerSkip(2))} + logger.UseLogLevel(zapcore.DebugLevel) + return logger + }), + ) + + if app.config.Modules != nil && len(app.config.Modules) > 0 { + for id, modConf := range app.config.Modules { + kind := modConf["kind"].(string) + module := modules.GetModule(kind) + if module == nil { + return ErrUnknownModule.Builder(). + Messagef("module '%s' is not registered", kind). + WithField("module", kind). + Build() + } + + option, err := module.Create(id, modConf) + if err != nil { + return err + } + + options = append(options, option) + } + } + + app.fx = fx.New(options...) + app.fx.Run() + return nil +} + +func (app *UniverseApplication) WithOption(options ...fx.Option) *UniverseApplication { + if app.started.Load() { + panic(ErrAlreadyStarted) + } + + app.options = append(app.options, options...) + return app +} + +func NewUniverseApplication(config *config.UniverseConfig) *UniverseApplication { + return &UniverseApplication{config: config} +} diff --git a/app/base_module.go b/app/base_module.go new file mode 100644 index 0000000..8326c71 --- /dev/null +++ b/app/base_module.go @@ -0,0 +1,53 @@ +package app + +import ( + "go.mrlokop.dev/universe/core/config" + "go.mrlokop.dev/universe/core/fs" + "go.mrlokop.dev/universe/core/modules" + "go.uber.org/zap" + "os" +) + +var ( + Module = modules.SimpleModule("universe:core", func(context modules.ModuleFactoryContext) error { + context.Invoke(func(log *zap.Logger, info *Info, env AppEnvType) { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + log.Sugar().Infof("Starting %s on %s, pid=%v, env=%s", info.ServiceName, hostname, os.Getpid(), env) + }) + context.Provide(func(env AppEnvType) RunPath { + if env == AppEnvTypeDevelopment { + return fs.System.INodeAt("run") + } + + runEnv := os.Getenv("UNIVERSE_RUN_PATH") + if runEnv == "" { + return fs.System.INodeAt(".") + } + return fs.System.INodeAt(runEnv) + }) + context.Provide(func(path RunPath) StoragePath { + return path.Join("storage") + }) + context.Provide(provideEnv) + context.Provide(provideAppInfo) + context.Provide(func(app Application) *config.UniverseConfig { + return app.Config() + }) + context.Provide(func(env AppEnvType) *zap.Logger { + if env == AppEnvTypeDevelopment { + config := zap.NewDevelopmentConfig() + config.OutputPaths = []string{"stdout"} + return zap.Must(config.Build( + zap.WithCaller(true), + )) + } + + return zap.Must(zap.NewProduction()) + }) + return nil + }) +) diff --git a/app/env.go b/app/env.go new file mode 100644 index 0000000..4e50ebf --- /dev/null +++ b/app/env.go @@ -0,0 +1,56 @@ +package app + +import ( + "os" +) + +type AppEnvType int +type AppEnvName string + +const ( + AppEnvTypeDevelopment AppEnvType = iota + AppEnvTypeStaging + AppEnvTypeProduction +) + +func (app AppEnvType) String() string { + switch app { + case AppEnvTypeDevelopment: + return "development" + case AppEnvTypeStaging: + return "staging" + case AppEnvTypeProduction: + return "production" + default: + return "unknown" + } +} + +var ( + knownEnvTypeEnvs = []string{ + "UNIVERSE_ENV", + "APP_ENV", + } +) + +func provideEnv() AppEnvType { + for _, envType := range knownEnvTypeEnvs { + value := os.Getenv(envType) + if value == "" { + continue + } + + switch envType { + case "development", "dev", "devel": + return AppEnvTypeDevelopment + case "staging", "test": + return AppEnvTypeStaging + case "production", "prod": + return AppEnvTypeProduction + default: + return AppEnvTypeDevelopment + } + } + + return AppEnvTypeDevelopment +} diff --git a/app/info.go b/app/info.go new file mode 100644 index 0000000..38f4024 --- /dev/null +++ b/app/info.go @@ -0,0 +1,44 @@ +package app + +import ( + "go.mrlokop.dev/universe/core/config" + "reflect" +) + +type Info struct { + ServiceName string + Version string + BuildInfo *BuildInfo +} + +type BuildInfo struct { + GitCommit string + BuildDate string +} + +func provideAppInfo( + app Application, + cfg *config.UniverseConfig, +) *Info { + serviceInfo := cfg.ServiceInfo + if serviceInfo == nil { + serviceInfo = &config.ServiceInfo{} + // extract service name from app + serviceInfo.Name = extractServiceName(app) + } + return &Info{ + ServiceName: serviceInfo.Name, + BuildInfo: &BuildInfo{ + GitCommit: "", + BuildDate: "", + }, + } +} + +func extractServiceName(app Application) string { + appType := reflect.TypeOf(app) + if appType.Kind() == reflect.Ptr { + return appType.Elem().Name() + } + return appType.Name() +} diff --git a/app/paths.go b/app/paths.go new file mode 100644 index 0000000..57eb545 --- /dev/null +++ b/app/paths.go @@ -0,0 +1,6 @@ +package app + +import "go.mrlokop.dev/universe/core/fs" + +type StoragePath fs.INode +type RunPath fs.INode diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..8294bfd --- /dev/null +++ b/app/utils.go @@ -0,0 +1,11 @@ +package app + +import "fmt" + +func Group(name string) string { + return fmt.Sprintf(`group:"%s"`, name) +} + +func Name(name string) string { + return fmt.Sprintf(`name:"%s"`, name) +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..7e8d754 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,37 @@ +package main + +import ( + ua "go.mrlokop.dev/universe/core/app" + uc "go.mrlokop.dev/universe/core/config" + "go.uber.org/fx" + "log" + "time" +) + +func main() { + runDirectory := "run" + + cfg := new(uc.UniverseConfig) + err := uc.LoadConfigFromDirectory(runDirectory, cfg) + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + app := ua.NewUniverseApplication(cfg) + + create, err := ua.Module.Create("$base", make(map[string]interface{})) + if err != nil { + log.Fatalf("Error creating base module: %v", err) + } + + app.WithOption( + fx.StartTimeout(10*time.Minute), + fx.Supply(cfg), + create, + ) + + err = app.Run() + if err != nil { + log.Fatalln(err.Error()) + } +} diff --git a/config/root.go b/config/root.go new file mode 100644 index 0000000..b103ae0 --- /dev/null +++ b/config/root.go @@ -0,0 +1,62 @@ +package config + +import ( + "errors" + "gopkg.in/yaml.v3" + "os" + "path/filepath" +) + +type UniverseConfig struct { + ServiceInfo *ServiceInfo `yaml:"service_info" json:"service_info" bson:"service_info"` + Modules map[string]map[string]interface{} `yaml:"modules" json:"modules" bson:"modules"` +} + +var ( + wellKnownConfigPlaces []string = []string{ + "config.yaml", + "config.yml", + } +) + +func LoadConfigFromDirectory[T any](dir string, out *T) error { + for _, fileName := range wellKnownConfigPlaces { + path := filepath.Join(dir, fileName) + if _, err := os.Stat(path); os.IsNotExist(err) { + continue + } + + return LoadConfigFromPath(path, out) + } + return nil +} + +func LoadConfigFromPath[T any](path string, out *T) error { + ext := filepath.Ext(path) + switch ext { + case ".yaml", ".yml": + // Load YAML config + if err := LoadYamlConfigFromPath(path, out); err != nil { + return err + } + return nil + + default: + return errors.New("Universe config file must be .yaml or .yml") + } +} + +func LoadYamlConfigFromPath[T any](path string, out *T) error { + file, err := os.Open(path) + if err != nil { + return err + } + + defer file.Close() + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(out); err != nil { + return err + } + + return nil +} diff --git a/config/service.go b/config/service.go new file mode 100644 index 0000000..026dd19 --- /dev/null +++ b/config/service.go @@ -0,0 +1,5 @@ +package config + +type ServiceInfo struct { + Name string `yaml:"name" json:"name" bson:"name"` +} diff --git a/errors/builder.go b/errors/builder.go new file mode 100644 index 0000000..a93aec9 --- /dev/null +++ b/errors/builder.go @@ -0,0 +1,69 @@ +package errors + +import "fmt" + +type UniverseErrorBuilder struct { + t *UniverseErrorType + message string + cause error + fields map[interface{}]any + stack ErrorStack +} + +func Builder() *UniverseErrorBuilder { + return &UniverseErrorBuilder{ + stack: CaptureStackIfEnabled(), + } +} + +func BuilderEmpty() *UniverseErrorBuilder { + return &UniverseErrorBuilder{} +} + +func (b *UniverseErrorBuilder) Message(message string) *UniverseErrorBuilder { + b.message = message + return b +} + +func (b *UniverseErrorBuilder) Messagef(format string, args ...interface{}) *UniverseErrorBuilder { + b.message = fmt.Sprintf(format, args...) + return b +} + +func (b *UniverseErrorBuilder) Type(typeName *UniverseErrorType) *UniverseErrorBuilder { + b.t = typeName + return b +} + +func (b *UniverseErrorBuilder) Cause(cause error) *UniverseErrorBuilder { + b.cause = cause + return b +} + +func (b *UniverseErrorBuilder) CaptureStackIfEnabled() *UniverseErrorBuilder { + b.stack = CaptureStackIfEnabled() + return b +} + +func (b *UniverseErrorBuilder) CaptureStack() *UniverseErrorBuilder { + b.stack = CaptureStack() + return b +} + +func (b *UniverseErrorBuilder) WithField(key interface{}, value interface{}) *UniverseErrorBuilder { + if b.fields == nil { + b.fields = make(map[interface{}]any) + } + b.fields[key] = value + return b +} + +func (b *UniverseErrorBuilder) Build() UniverseError { + return &UniverseErrorImpl{ + msg: b.message, + cause: b.cause, + fields: b.fields, + stack: b.stack, + t: b.t, + } +} diff --git a/errors/error.go b/errors/error.go new file mode 100644 index 0000000..bc28c42 --- /dev/null +++ b/errors/error.go @@ -0,0 +1,72 @@ +package errors + +type UniverseError interface { + Error() string + + Type() *UniverseErrorType + Message() string + Cause() error + StackTrace() ErrorStack + + GetAny(key interface{}) any + With(key interface{}, value any) UniverseError + Builder() *UniverseErrorBuilder +} + +type UniverseErrorImpl struct { + msg string + cause error + t *UniverseErrorType + stack ErrorStack + fields map[interface{}]any +} + +func (e *UniverseErrorImpl) Type() *UniverseErrorType { + return e.t +} + +func (e *UniverseErrorImpl) StackTrace() ErrorStack { + return e.stack +} + +func (e *UniverseErrorImpl) With(field interface{}, value any) UniverseError { + if e.fields == nil { + e.fields = map[interface{}]any{} + } + e.fields[field] = value + return e +} + +func (e *UniverseErrorImpl) Builder() *UniverseErrorBuilder { + return &UniverseErrorBuilder{ + t: e.t, + message: e.msg, + cause: e.cause, + fields: e.fields, + stack: e.stack, + } +} + +func (e *UniverseErrorImpl) Error() string { + return ErrorToString(e) +} + +func (e *UniverseErrorImpl) Unwrap() error { + return e.cause +} + +func (e *UniverseErrorImpl) Message() string { + return e.msg +} + +func (e *UniverseErrorImpl) Cause() error { + return e.cause +} + +func (e *UniverseErrorImpl) GetAny(field interface{}) any { + if e.fields == nil { + return nil + } + + return e.fields[field] +} diff --git a/errors/printer.go b/errors/printer.go new file mode 100644 index 0000000..afacc76 --- /dev/null +++ b/errors/printer.go @@ -0,0 +1,47 @@ +package errors + +import ( + "io" + "strings" +) + +func ErrorToString(e error) string { + builder := &strings.Builder{} + AppendError(builder, e, 0) + return builder.String() +} + +func AppendError(writer io.StringWriter, err error, depth uint) { + padding := strings.Repeat(" ", int(depth)) + + var uniError UniverseError + if As(err, &uniError) { + errorType := uniError.Type() + if errorType != nil { + _, _ = writer.WriteString(errorType.Name) + _, _ = writer.WriteString(": ") + } + + _, _ = writer.WriteString(uniError.Message()) + + stack := uniError.StackTrace() + if stack != nil { + for _, value := range stack { + _, _ = writer.WriteString("\n") + _, _ = writer.WriteString(padding) + _, _ = writer.WriteString(value) + } + } + + cause := uniError.Cause() + if cause != nil { + _, _ = writer.WriteString("\n") + _, _ = writer.WriteString(padding) + _, _ = writer.WriteString("Caused by: ") + AppendError(writer, cause, depth+1) + } + + } else { + _, _ = writer.WriteString(err.Error()) + } +} diff --git a/errors/stack.go b/errors/stack.go new file mode 100644 index 0000000..65c4738 --- /dev/null +++ b/errors/stack.go @@ -0,0 +1,27 @@ +package errors + +import ( + "runtime" + "strings" +) + +var ( + IsStackCaptureEnabled = true + StackCaptureSize = 1024 * 4 +) + +type ErrorStack = []string + +func CaptureStackIfEnabled() ErrorStack { + if !IsStackCaptureEnabled { + return nil + } + + return CaptureStack() +} + +func CaptureStack() ErrorStack { + buffer := make([]byte, StackCaptureSize) + stackSize := runtime.Stack(buffer, false) + return strings.Split(string(buffer[:stackSize]), "\n") +} diff --git a/errors/type.go b/errors/type.go new file mode 100644 index 0000000..141a935 --- /dev/null +++ b/errors/type.go @@ -0,0 +1,84 @@ +package errors + +import "fmt" + +type UniverseErrorType struct { + error + + ParentType *UniverseErrorType + Name string + DefaultMessage string +} + +func (e *UniverseErrorType) Builder() *UniverseErrorBuilder { + return &UniverseErrorBuilder{ + t: e, + stack: CaptureStackIfEnabled(), + } +} + +func (e *UniverseErrorType) CreateWithCause(cause error) UniverseError { + return &UniverseErrorImpl{ + t: e, + cause: cause, + msg: e.DefaultMessage, + stack: CaptureStackIfEnabled(), + } +} + +func (e *UniverseErrorType) CreateWithMessage(message string) UniverseError { + return &UniverseErrorImpl{ + t: e, + msg: message, + stack: CaptureStackIfEnabled(), + } +} + +func (e *UniverseErrorType) Create(message string, cause error) UniverseError { + return &UniverseErrorImpl{ + t: e, + msg: message, + cause: cause, + stack: CaptureStackIfEnabled(), + } +} + +func (e *UniverseErrorType) CreateEmpty() UniverseError { + return &UniverseErrorImpl{ + t: e, + msg: e.DefaultMessage, + stack: CaptureStackIfEnabled(), + } +} + +func (e *UniverseErrorType) Is(err error) bool { + return Is(err, e) +} + +func (e *UniverseErrorType) Child(childName string) *UniverseErrorType { + return &UniverseErrorType{ + ParentType: e, + Name: childName, + } +} + +func (e *UniverseErrorType) Error() string { + return fmt.Sprintf("type: %s", e.Name) +} + +func (e *UniverseErrorType) Panic() { + panic(e.CreateEmpty()) +} + +func Type(name string, defaultMessage string) *UniverseErrorType { + return &UniverseErrorType{ + Name: name, + DefaultMessage: defaultMessage, + } +} + +func New(message string) *UniverseErrorType { + return &UniverseErrorType{ + DefaultMessage: message, + } +} diff --git a/errors/utils.go b/errors/utils.go new file mode 100644 index 0000000..a8038b7 --- /dev/null +++ b/errors/utils.go @@ -0,0 +1,127 @@ +package errors + +import ( + "reflect" +) + +func Wrap(err error, message string) error { + return &UniverseErrorImpl{ + msg: message, + cause: err, + stack: CaptureStackIfEnabled(), + } +} + +func Get[T *any](err error, field string) T { + if err == nil { + return nil + } + + var uerr UniverseError + if As(err, &uerr) { + value := uerr.GetAny(field) + if value, ok := value.(T); ok { + return value + } + } + + return nil +} + +func Unwrap(err error) error { + u, ok := err.(interface { + Unwrap() error + }) + if !ok { + return nil + } + return u.Unwrap() +} + +func Is(err, target error) bool { + if err == nil || target == nil { + return err == target + } + + isComparable := reflect.TypeOf(target).Comparable() + return is(err, target, isComparable) +} + +func is(err, target error, targetComparable bool) bool { + for { + if targetComparable && err == target { + return true + } + if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { + return true + } + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return false + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if is(err, target, targetComparable) { + return true + } + } + return false + default: + return false + } + } +} + +func As(err error, target any) bool { + if err == nil { + return false + } + if target == nil { + panic("errors: target cannot be nil") + } + val := reflect.ValueOf(target) + typ := val.Type() + if typ.Kind() != reflect.Ptr || val.IsNil() { + panic("errors: target must be a non-nil pointer") + } + targetType := typ.Elem() + if targetType.Kind() != reflect.Interface && !targetType.Implements(errorType) { + panic("errors: *target must be interface or implement error") + } + return as(err, target, val, targetType) +} + +func as(err error, target any, targetVal reflect.Value, targetType reflect.Type) bool { + for { + if reflect.TypeOf(err).AssignableTo(targetType) { + targetVal.Elem().Set(reflect.ValueOf(err)) + return true + } + if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) { + return true + } + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return false + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if err == nil { + continue + } + if as(err, target, targetVal, targetType) { + return true + } + } + return false + default: + return false + } + } +} + +var errorType = reflect.TypeOf((*error)(nil)).Elem() diff --git a/fs/fs.go b/fs/fs.go new file mode 100644 index 0000000..77bb24d --- /dev/null +++ b/fs/fs.go @@ -0,0 +1,28 @@ +package fs + +import ( + "io" + "os" +) + +type FileSystem interface { + INodeAt(path string) INode +} + +type INode interface { + IsExists() bool + Stat() (os.FileInfo, error) + + MakeDirs() error + RemoveAll() error + + Parent() INode + Join(path ...string) INode + + ReadIntoBytes() ([]byte, error) + OpenReadStream() (io.ReadCloser, error) + OpenWriteStream() (io.WriteCloser, error) + + Name() string + AbsolutePath() string +} diff --git a/fs/sys.go b/fs/sys.go new file mode 100644 index 0000000..ee0005b --- /dev/null +++ b/fs/sys.go @@ -0,0 +1,83 @@ +package fs + +import ( + "io" + "os" + "path/filepath" +) + +type sys struct { +} + +type sysInode struct { + path string +} + +func (s *sysInode) RemoveAll() error { + return os.RemoveAll(s.path) +} + +func (s *sysInode) Join(path ...string) INode { + return &sysInode{ + path: filepath.Join(path...), + } +} + +func (s *sysInode) MakeDirs() error { + return os.MkdirAll(s.path, 0755) +} + +func (s *sysInode) Parent() INode { + return &sysInode{ + path: filepath.Dir(s.path), + } +} + +func (s *sysInode) ReadIntoBytes() ([]byte, error) { + return os.ReadFile(s.path) +} + +func (s *sysInode) OpenReadStream() (io.ReadCloser, error) { + f, err := os.Open(s.path) + if err != nil { + return nil, err + } + return f, nil +} + +func (s *sysInode) OpenWriteStream() (io.WriteCloser, error) { + return os.OpenFile(s.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) +} + +func (s *sysInode) Stat() (os.FileInfo, error) { + return os.Stat(s.path) +} + +func (s *sysInode) IsExists() bool { + if _, err := os.Stat(s.path); os.IsNotExist(err) { + return false + } + return true +} + +func (s *sysInode) Name() string { + return filepath.Base(s.path) +} + +func (s *sysInode) AbsolutePath() string { + abs, err := filepath.Abs(s.path) + if err != nil { + return s.path + } + return abs +} + +func (s *sys) INodeAt(path string) INode { + return &sysInode{ + path: path, + } +} + +var ( + System FileSystem = &sys{} +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..54a2cbf --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module go.mrlokop.dev/universe/core + +go 1.24 + +require ( + github.com/mitchellh/mapstructure v1.5.0 + go.uber.org/fx v1.23.0 + go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/kr/pretty v0.3.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/sys v0.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1678aa3 --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/module.go b/modules/module.go new file mode 100644 index 0000000..ae9f640 --- /dev/null +++ b/modules/module.go @@ -0,0 +1,120 @@ +package modules + +import ( + "fmt" + "github.com/mitchellh/mapstructure" + "go.uber.org/fx" + "go.uber.org/zap" +) + +type ModuleConfiguration interface { + Extract(destination any) error +} + +var registry map[string]Module = map[string]Module{} + +func RegisterModule(module Module) { + registry[module.Name()] = module +} + +func GetModule(name string) Module { + return registry[name] +} + +type Module interface { + Name() string + + Create(id string, moduleConfiguration map[string]interface{}) (fx.Option, error) +} + +type ModuleFactoryContext interface { + Id() string + Kind() string + + NameTag() string + GroupTag() string + + ConfigureLogger(logger *zap.Logger) *zap.Logger + + ExtractConfig(destination any) error + AddOption(option fx.Option) + + Provide(constructors ...interface{}) + Invoke(funcs ...interface{}) +} + +type simpleFactoryContext struct { + id string + kind string + config map[string]interface{} + options []fx.Option +} + +func (s *simpleFactoryContext) Kind() string { + return s.kind +} + +func (s *simpleFactoryContext) ConfigureLogger(logger *zap.Logger) *zap.Logger { + return logger.Named(fmt.Sprintf("%s[%s]", s.Kind(), s.id)) +} + +func (s *simpleFactoryContext) Provide(constructors ...interface{}) { + s.AddOption(fx.Provide(constructors...)) +} + +func (s *simpleFactoryContext) Invoke(funcs ...interface{}) { + s.AddOption(fx.Invoke(funcs...)) +} + +func (s *simpleFactoryContext) Id() string { + return s.id +} + +func (s *simpleFactoryContext) NameTag() string { + return fmt.Sprintf(`name:"%s"`, s.id) +} + +func (s *simpleFactoryContext) GroupTag() string { + return fmt.Sprintf(`group:"%s"`, s.id) +} + +func (s *simpleFactoryContext) ExtractConfig(destination any) error { + return mapstructure.Decode(s.config, destination) +} + +func (s *simpleFactoryContext) AddOption(option fx.Option) { + s.options = append(s.options, option) +} + +type simpleFactory = func(context ModuleFactoryContext) error + +type simple struct { + name string + factory simpleFactory +} + +func (s simple) Name() string { + return s.name +} + +func (s simple) Create(id string, config map[string]interface{}) (fx.Option, error) { + ctx := &simpleFactoryContext{ + options: make([]fx.Option, 0), + config: config, + id: id, + kind: s.name, + } + + err := s.factory(ctx) + if err != nil { + return nil, err + } + + return fx.Options(ctx.options...), nil +} + +func SimpleModule(name string, factory simpleFactory) Module { + module := &simple{name: name, factory: factory} + RegisterModule(module) + return module +} diff --git a/utils/fn.go b/utils/fn.go new file mode 100644 index 0000000..e8cecab --- /dev/null +++ b/utils/fn.go @@ -0,0 +1,4 @@ +package utils + +type Consumer[T any] func(T) +type Supplier[T any] func() T