package main import ( "context" "encoding/json" "fmt" "log/slog" "os" "git.loyso.art/frx/kurious/internal/common/client/sravni" "git.loyso.art/frx/kurious/internal/common/config" "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/kurious/adapters" "github.com/teris-io/cli" ) var ( limitOption = cli.NewOption("limit", "Limits amount of items to return").WithType(cli.TypeInt) offsetOption = cli.NewOption("offset", "Offsets items to return").WithType(cli.TypeInt) debugOption = cli.NewOption("verbose", "Enables debug logging").WithChar('v').WithType(cli.TypeBool) jsonOption = cli.NewOption("json", "Sets output as json").WithType(cli.TypeBool) ) type actionWrapper func(next cli.Action) cli.Action func buildCLICommand(f func() cli.Command) cliCommand { return makeCLICommand(f(), actionWrapperParseConfig) } func makeCLICommand(cmd cli.Command, wrappers ...actionWrapper) cliCommand { return cliCommand{ Command: cmd, actionWrappers: wrappers, } } type cliCommand struct { cli.Command actionWrappers []actionWrapper } // WithCommand is disabled since it adds action wrappers and // not tested in other cases. func (c cliCommand) WithCommand(cli.Command) cli.Command { panic("wrapped in cliCommand is expected to be leaf command") } func (c cliCommand) Action() cli.Action { out := c.Command.Action() if out == nil { return nil } for _, wrapper := range c.actionWrappers { out = wrapper(out) } return out } func actionWrapperParseConfig(next cli.Action) cli.Action { return func(args []string, options map[string]string) int { var result int parseConfigOnce.Do(func() { cfgpath, ok := options["config"] if !ok || cfgpath == "" { cfgpath = defaultConfigPath } payload, err := os.ReadFile(cfgpath) if err != nil { slog.Error("unable to read config file", slog.Any("err", err)) result = -1 return } err = json.Unmarshal(payload, ¤tConfig) if err != nil { slog.Error("unable to unmarshal config file", slog.Any("err", err)) result = -1 return } }) if result != 0 { return result } return next(args, options) } } func isJSONFormatEnabled(options map[string]string) bool { _, ok := options[jsonOption.Key()] return ok } type outputEncoderF func(any) error func makeOutputEncoder(options map[string]string) outputEncoderF { if isJSONFormatEnabled(options) { out := json.NewEncoder(defaultOutput) out.SetIndent("", " ") return out.Encode } return outputEncoderF(func(a any) error { _, err := fmt.Fprintf(defaultOutput, "%#v", a) return err }) } func makeLogger(options map[string]string) *slog.Logger { level := slog.LevelInfo if _, ok := options[debugOption.Key()]; ok { level = slog.LevelDebug } opts := slog.HandlerOptions{ Level: level, } var h slog.Handler if isJSONFormatEnabled(options) { h = slog.NewJSONHandler(os.Stdout, &opts) } else { h = slog.NewTextHandler(os.Stdout, &opts) } return slog.New(h) } func makeSravniClient(ctx context.Context, log *slog.Logger, options map[string]string) (sravni.Client, error) { _, isDebug := options[debugOption.Key()] client, err := sravni.NewClient(ctx, log, isDebug) if err != nil { return nil, fmt.Errorf("making new client: %w", err) } return client, nil } type baseAction struct { ctx context.Context log *slog.Logger out outputEncoderF } func (ba *baseAction) getYDBConnection() (*adapters.YDBConnection, error) { if currentConfig.YDB == (config.YDB{}) { return nil, errors.SimpleError("no ydb config set") } ydbConn, err := adapters.NewYDBConnection(ba.ctx, currentConfig.YDB, ba.log.With(slog.String("db", "ydb"))) if err != nil { return nil, fmt.Errorf("making new ydb course repository: %w", err) } return ydbConn, nil } func (ba *baseAction) parse(_ []string, options map[string]string) (err error) { ba.log = makeLogger(options).With(slog.String("component", "action")) ba.out = makeOutputEncoder(options) return nil } func (ba *baseAction) handle() error { return errors.ErrNotImplemented } func (ba *baseAction) context() context.Context { return ba.ctx } func newBaseAction(ctx context.Context) *baseAction { return &baseAction{ ctx: ctx, } }