feat: unifood base with GET dishes (#1)

Features:
- CLI structure with verbs and resources
- Application config and parameters
- Output formatters
- Initial resource: dishes

Reviewed-on: #1
Co-authored-by: bdoerfchen <git@bissendorf.co>
Co-committed-by: bdoerfchen <git@bissendorf.co>
This commit is contained in:
2025-07-20 17:29:04 +00:00
committed by bissendorf
parent ad082a3f12
commit ec66365b5e
27 changed files with 893 additions and 0 deletions

7
cmd/config.go Normal file
View File

@ -0,0 +1,7 @@
package cmd
type AppConfig struct {
OutputVerbose bool
OutputFormatter string
PrintConfig bool
}

19
cmd/resources.go Normal file
View File

@ -0,0 +1,19 @@
package cmd
import (
"git.bissendorf.co/bissendorf/unifood/m/v2/core/handler/dishes"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/services/stwhbclient"
"git.bissendorf.co/bissendorf/unifood/m/v2/model/external/stwbremen"
)
var availableResources = []interfaces.ResourceCommand[any]{
{
Name: "dishes",
Aliases: []string{"dish", "d"},
Verbs: []interfaces.Verb{interfaces.VerbGet},
Handler: &dishes.DishesHandler{
QueryClient: stwhbclient.New[[]stwbremen.Dish](),
},
},
}

41
cmd/root.go Normal file
View File

@ -0,0 +1,41 @@
package cmd
import (
"context"
"fmt"
"log/slog"
"os"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/services/jlog"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "unifood",
Short: "Unifood is a CLI for retrieving restaurant information",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
},
}
func Execute() {
initRootCmd()
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func initRootCmd() {
var appConfig AppConfig
rootCmd.PersistentFlags().BoolVarP(&appConfig.OutputVerbose, "verbose", "v", false, "Enable verbose output")
rootCmd.PersistentFlags().StringVarP(&appConfig.OutputFormatter, "output", "o", "table", "Set output format")
rootCmd.PersistentFlags().BoolVar(&appConfig.PrintConfig, "print-config", false, "Enable printing the application config")
logger := jlog.New(slog.LevelDebug)
ctx := jlog.ContextWith(context.Background(), logger)
rootCmd.AddCommand(getVerbs(ctx, &appConfig)...)
}

122
cmd/verbs.go Normal file
View File

@ -0,0 +1,122 @@
package cmd
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"slices"
"strings"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces/params"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/output"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/services/jlog"
"github.com/spf13/cobra"
)
type VerbItem struct {
Name interfaces.Verb
Aliases []string
Description string
RunFn func(ctx context.Context, config *AppConfig, handler interfaces.ResourceHandler, params params.Container) error
}
var verbs = []VerbItem{
{
Name: interfaces.VerbGet,
Description: "Retrieve a list of resources",
RunFn: func(ctx context.Context, config *AppConfig, handler interfaces.ResourceHandler, params params.Container) error {
h, ok := handler.(interfaces.GetHandler)
if !ok {
return fmt.Errorf("resource does not support GET")
}
// Get items
items, err := h.Get(ctx, params)
if err != nil {
return fmt.Errorf("retrieving item failed: %w", err)
}
formatterName := strings.ToLower(config.OutputFormatter)
formatter, exists := output.Formatters[formatterName]
if !exists {
return fmt.Errorf("could not find output formatter '%s'", formatterName)
}
// Format and output
formatted, err := formatter.Format(items)
if err != nil {
return fmt.Errorf("failed to format output: %w", err)
}
_, err = io.Copy(os.Stdout, formatted)
if err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return nil
},
},
}
func getVerbs(ctx context.Context, config *AppConfig) (commands []*cobra.Command) {
for _, v := range verbs {
verbCommand := &cobra.Command{
Use: strings.ToLower(string(v.Name)),
Aliases: v.Aliases,
Short: v.Description,
}
// Add all resources that can handle this verb
for _, r := range availableResources {
if !slices.Contains(r.Verbs, v.Name) {
continue
}
var params params.Container
resourceCommand := &cobra.Command{
Use: r.Name,
Aliases: r.Aliases,
Short: r.Description,
Run: func(cmd *cobra.Command, args []string) {
// Configure log
var logLevel = slog.LevelWarn
if config.OutputVerbose {
logLevel = slog.LevelDebug
}
logger := jlog.New(logLevel)
ctx := jlog.ContextWith(context.Background(), logger)
// Print config
if config.PrintConfig {
logger.WarnContext(ctx, "Printing app config", slog.Any("config", config), slog.Any("parameters", params.ToMap()))
}
err := v.RunFn(ctx, config, r.Handler, params)
if err != nil {
logger.ErrorContext(ctx, fmt.Sprintf("%s %s failed", strings.ToUpper(string(v.Name)), r.Name), "error", err.Error())
os.Exit(1)
}
},
}
// Register parameters
for _, param := range r.Handler.GetParametersForVerb(v.Name) {
resourceCommand.Flags().StringVarP(
params.Register(param.Name, param.ParseFunc),
param.Name,
param.ShortHand,
param.DefaultFunc(),
param.Description,
)
}
verbCommand.AddCommand(resourceCommand)
}
commands = append(commands, verbCommand)
}
return
}