Compare commits
6 Commits
4a15a4db66
...
v0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 723aae48ce | |||
| 90a5125c66 | |||
| 4b7866da03 | |||
| e395b0b4ca | |||
| da2d507629 | |||
| ec66365b5e |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
unifood
|
||||
__bin*
|
||||
__debug*
|
||||
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug app",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": [
|
||||
"get", "meals",
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
8
cmd/config.go
Normal file
8
cmd/config.go
Normal file
@ -0,0 +1,8 @@
|
||||
package cmd
|
||||
|
||||
type AppConfig struct {
|
||||
OutputVerbose bool
|
||||
OutputFormatter string
|
||||
OutputOrderReverse bool
|
||||
PrintConfig bool
|
||||
}
|
||||
82
cmd/resources.go
Normal file
82
cmd/resources.go
Normal file
@ -0,0 +1,82 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/handler/meals"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/handler/universities"
|
||||
"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/services/stwhbclient"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/external/stwbremen"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/resources"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
)
|
||||
|
||||
var availableResources = []interfaces.ResourceCommand[any]{
|
||||
{
|
||||
Name: "resources",
|
||||
Aliases: []string{"resource"},
|
||||
Description: "A meta resource representing all other object kinds of this CLI.",
|
||||
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
||||
Handler: ®isteredResourcesHandler{},
|
||||
},
|
||||
{
|
||||
Name: "meals",
|
||||
Aliases: []string{"meal", "m"},
|
||||
Description: "A meal represents a cooked combination of ingredients that can be bought and consumed.",
|
||||
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
||||
Handler: &meals.MealsHandler{
|
||||
QueryClient: stwhbclient.New[[]stwbremen.Meal](),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "universities",
|
||||
Aliases: []string{"university", "unis", "uni", "u"},
|
||||
Description: "A facility that is hosting one or multiple restaurants.",
|
||||
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
||||
Handler: &universities.UniversityHandler{
|
||||
QueryClient: stwhbclient.New[stwbremen.RestaurantList](),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "restaurants",
|
||||
Aliases: []string{"restaurant", "r"},
|
||||
Description: "A place to eat meals",
|
||||
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
||||
Handler: &universities.RestaurantHandler{
|
||||
QueryClient: stwhbclient.New[stwbremen.RestaurantList](),
|
||||
QueryClientRestaurant: stwhbclient.New[stwbremen.Restaurant](),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type registeredResourcesHandler struct{}
|
||||
|
||||
func (h *registeredResourcesHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) {
|
||||
lowerName := strings.ToLower(name)
|
||||
list := util.Select(availableResources, func(i *interfaces.ResourceCommand[any]) bool {
|
||||
return name == "" || i.Name == lowerName
|
||||
})
|
||||
|
||||
slices.SortFunc(list, func(a, b interfaces.ResourceCommand[any]) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return &interfaces.ResourceList{
|
||||
ItemKind: resources.ResourceResource,
|
||||
Items: util.Transform(list, func(i *interfaces.ResourceCommand[any]) interfaces.Resource {
|
||||
return &resources.Resource{
|
||||
ResourceName: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Description: i.Description,
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *registeredResourcesHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
|
||||
return []params.Registration{}
|
||||
}
|
||||
43
cmd/root.go
Normal file
43
cmd/root.go
Normal file
@ -0,0 +1,43 @@
|
||||
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.OutputOrderReverse, "reverse", false, "Reverses output item order")
|
||||
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(&versionCmd)
|
||||
rootCmd.AddCommand(getVerbs(ctx, &appConfig)...)
|
||||
}
|
||||
132
cmd/verbs.go
Normal file
132
cmd/verbs.go
Normal file
@ -0,0 +1,132 @@
|
||||
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, name string) 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, name string) error {
|
||||
h, ok := handler.(interfaces.GetHandler)
|
||||
if !ok {
|
||||
return fmt.Errorf("resource does not support GET")
|
||||
}
|
||||
|
||||
// Get items
|
||||
items, err := h.Get(ctx, name, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving item failed: %w", err)
|
||||
}
|
||||
if config.OutputOrderReverse {
|
||||
slices.Reverse(items.Items)
|
||||
}
|
||||
|
||||
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,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
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()))
|
||||
}
|
||||
|
||||
var name string
|
||||
if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
|
||||
err := v.RunFn(ctx, config, r.Handler, params, name)
|
||||
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
|
||||
}
|
||||
16
cmd/version.go
Normal file
16
cmd/version.go
Normal file
@ -0,0 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var versionCmd = cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"v"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(util.AppVersion)
|
||||
},
|
||||
}
|
||||
108
core/handler/meals/handler.go
Normal file
108
core/handler/meals/handler.go
Normal file
@ -0,0 +1,108 @@
|
||||
package meals
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/handler"
|
||||
"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/model/external/stwbremen"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/resources"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
)
|
||||
|
||||
type MealsHandler struct {
|
||||
interfaces.ResourceHandler
|
||||
interfaces.GetHandler
|
||||
|
||||
QueryClient interfaces.QueryClient[[]stwbremen.Meal]
|
||||
}
|
||||
|
||||
func (h *MealsHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) {
|
||||
// Read parameters
|
||||
date, err := params.GetValue(handler.ParamDate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse date parameter: %w", err)
|
||||
}
|
||||
|
||||
restaurant, err := params.GetValue(handler.ParamRestaurant)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse restaurant parameter: %w", err)
|
||||
}
|
||||
|
||||
// Build query
|
||||
query := "page('meals').children.filterBy('printonly', 0)"
|
||||
if date != "" {
|
||||
parsed, err := time.Parse(time.DateOnly, date.(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse date parameter: %w", err)
|
||||
}
|
||||
query += fmt.Sprintf(".filterBy('date', '%s')", parsed.Format(time.DateOnly))
|
||||
}
|
||||
if restaurant != "" {
|
||||
query += fmt.Sprintf(".filterBy('location', '%s')", restaurant.(string))
|
||||
}
|
||||
if name != "" {
|
||||
query += fmt.Sprintf(".filterBy('id', 'meals/%s')", name)
|
||||
}
|
||||
|
||||
// Run query
|
||||
meals, err := h.QueryClient.Get(ctx,
|
||||
query,
|
||||
`{"title":true,"id":"page.id","ingredients":"page.ingredients.toObject","prices":"page.prices.toObject","location":true,"counter":true,"date":true,"mealadds":true,"mark":true,"frei3":true,"printonly":true,"kombicategory":true,"categories":"page.categories.split"}`,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying menu failed: %w", err)
|
||||
}
|
||||
|
||||
// Transform to meal resource
|
||||
items := util.Transform(*meals, func(i *stwbremen.Meal) *resources.Meal {
|
||||
d, err := resources.MealFromDTO(*i)
|
||||
if err != nil {
|
||||
return &resources.Meal{}
|
||||
}
|
||||
|
||||
return d
|
||||
})
|
||||
|
||||
// Sort by date, restaurant and title - need to apply stable sorts in reverse order to achieve this
|
||||
slices.SortFunc(items, func(a, b *resources.Meal) int {
|
||||
return strings.Compare(a.Title, b.Title)
|
||||
})
|
||||
slices.SortStableFunc(items, func(a, b *resources.Meal) int {
|
||||
return strings.Compare(a.RestaurantID, b.RestaurantID)
|
||||
})
|
||||
slices.SortStableFunc(items, func(a, b *resources.Meal) int {
|
||||
return a.Date.Compare(b.Date)
|
||||
})
|
||||
|
||||
// Return
|
||||
return &interfaces.ResourceList{
|
||||
ItemKind: resources.ResourceMeal,
|
||||
Items: util.Transform(items, func(i **resources.Meal) interfaces.Resource { return *i }),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *MealsHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
|
||||
return []params.Registration{
|
||||
{
|
||||
Name: handler.ParamDate,
|
||||
ShortHand: "d",
|
||||
Description: "Meal date",
|
||||
DefaultFunc: func() string { return "" },
|
||||
ParseFunc: params.ParseString,
|
||||
},
|
||||
{
|
||||
Name: handler.ParamRestaurant,
|
||||
ShortHand: "r",
|
||||
Description: "Select a restaurant",
|
||||
DefaultFunc: func() string { return "" },
|
||||
ParseFunc: params.ParseString,
|
||||
},
|
||||
}
|
||||
}
|
||||
6
core/handler/params.go
Normal file
6
core/handler/params.go
Normal file
@ -0,0 +1,6 @@
|
||||
package handler
|
||||
|
||||
const (
|
||||
ParamDate string = "date"
|
||||
ParamRestaurant string = "restaurant"
|
||||
)
|
||||
93
core/handler/universities/restaurant-handler.go
Normal file
93
core/handler/universities/restaurant-handler.go
Normal file
@ -0,0 +1,93 @@
|
||||
package universities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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/model/external/stwbremen"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/resources"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
)
|
||||
|
||||
type RestaurantHandler struct {
|
||||
QueryClient interfaces.QueryClient[stwbremen.RestaurantList]
|
||||
QueryClientRestaurant interfaces.QueryClient[stwbremen.Restaurant]
|
||||
}
|
||||
|
||||
func (h *RestaurantHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) {
|
||||
var list []string = []string{name}
|
||||
|
||||
// If no name provided, get list of all available restaurants
|
||||
if name == "" {
|
||||
fetchedNames, err := getRestaurantList(ctx, h.QueryClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list = fetchedNames
|
||||
}
|
||||
|
||||
// Get restaurant information in parallel
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
restaurants := make([]resources.Restaurant, 0, len(list))
|
||||
for _, name := range list {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
restaurant, err := h.getRestaurantInfo(ctx, name)
|
||||
|
||||
// Append restaurant in critical section
|
||||
if err == nil {
|
||||
mutex.Lock()
|
||||
restaurants = append(restaurants, *restaurant)
|
||||
mutex.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Sort by name
|
||||
slices.SortFunc(restaurants, func(a, b resources.Restaurant) int { return strings.Compare(a.Name, b.Name) })
|
||||
|
||||
// Return
|
||||
return &interfaces.ResourceList{
|
||||
ItemKind: resources.ResourceRestaurant,
|
||||
Items: util.Transform(restaurants, func(i *resources.Restaurant) interfaces.Resource { return i }),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (h *RestaurantHandler) getRestaurantInfo(ctx context.Context, name string) (*resources.Restaurant, error) {
|
||||
r, err := h.QueryClientRestaurant.Get(ctx,
|
||||
fmt.Sprintf("page('essen-und-trinken/%s')", name),
|
||||
`{"title": "page.title","id": "page.content.mensalocationid","image": "page.files.first().url","address": "page.content.address","openingTimes": {"query": "page.openingTimes.toStructure()","select": {"weekday": "structureItem.day","openingTime": "structureItem.open","closingTime": "structureItem.close"}},"offseasonOpeningTimes": {"query": "page.offseasonOpeningTimes.toStructure()","select": {"weekday": "structureItem.day","openingTime": "structureItem.open","closingTime": "structureItem.close"}},"offseasonStart": true,"offseasonEnd": true,"changedTimes": {"query": "page.changedTimes.toStructure()","select": {"startDate": "structureItem.startChangedDate","endDate": "structureItem.endChangedDate","openingTime": "structureItem.openChangedTime","closingTime": "structureItem.closedChangedTime"}}}`,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load restaurant %s: %w", name, err)
|
||||
}
|
||||
|
||||
if r.Title == "" {
|
||||
return nil, fmt.Errorf("restaurant not found: %s", name)
|
||||
}
|
||||
|
||||
return &resources.Restaurant{
|
||||
Name: name,
|
||||
Title: r.Title,
|
||||
ID: r.ID,
|
||||
Address: r.Address,
|
||||
Image: r.Image,
|
||||
OpeningHours: r.OpeningHours,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *RestaurantHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
|
||||
return []params.Registration{}
|
||||
}
|
||||
31
core/handler/universities/shared.go
Normal file
31
core/handler/universities/shared.go
Normal file
@ -0,0 +1,31 @@
|
||||
package universities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/external/stwbremen"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
)
|
||||
|
||||
func getRestaurantList(ctx context.Context, client interfaces.QueryClient[stwbremen.RestaurantList]) ([]string, error) {
|
||||
list, err := client.Get(ctx,
|
||||
`page('essen-und-trinken')`,
|
||||
`{"items": "page.children.children"}`,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load university list: %w", err)
|
||||
}
|
||||
|
||||
return util.Transform(list.Items, func(i *string) string {
|
||||
parts := strings.SplitN(*i, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return parts[1]
|
||||
}), nil
|
||||
}
|
||||
64
core/handler/universities/uni-handler.go
Normal file
64
core/handler/universities/uni-handler.go
Normal file
@ -0,0 +1,64 @@
|
||||
package universities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/model/external/stwbremen"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/resources"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
)
|
||||
|
||||
type UniversityHandler struct {
|
||||
QueryClient interfaces.QueryClient[stwbremen.RestaurantList]
|
||||
}
|
||||
|
||||
func (h *UniversityHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) {
|
||||
// Get list of restaurants
|
||||
list, err := getRestaurantList(ctx, h.QueryClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Group restaurants into universities
|
||||
unis := util.Group(list, func(i *string) (string, string) {
|
||||
parts := strings.SplitN(*i, "/", 2)
|
||||
if len(parts) <= 1 {
|
||||
return *i, *i
|
||||
}
|
||||
|
||||
return parts[0], *i
|
||||
})
|
||||
|
||||
// If name is provided remove all other unis
|
||||
if name != "" {
|
||||
uni, exists := unis[name]
|
||||
unis = make(map[string][]string)
|
||||
|
||||
if exists {
|
||||
unis[name] = uni
|
||||
}
|
||||
}
|
||||
|
||||
// Map into resource
|
||||
uniItems := make([]resources.University, 0, len(unis))
|
||||
for uni, restaurants := range unis {
|
||||
uniItems = append(uniItems, resources.University{
|
||||
Name: uni,
|
||||
Restaurants: restaurants,
|
||||
})
|
||||
}
|
||||
|
||||
// Return
|
||||
return &interfaces.ResourceList{
|
||||
ItemKind: resources.ResourceUniversity,
|
||||
Items: util.Transform(uniItems, func(i *resources.University) interfaces.Resource { return i }),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (h *UniversityHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
|
||||
return []params.Registration{}
|
||||
}
|
||||
29
core/interfaces/handler.go
Normal file
29
core/interfaces/handler.go
Normal file
@ -0,0 +1,29 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces/params"
|
||||
)
|
||||
|
||||
type ResourceCommand[T any] struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Description string
|
||||
Verbs []Verb
|
||||
Handler ResourceHandler
|
||||
}
|
||||
|
||||
type ResourceHandler interface {
|
||||
GetParametersForVerb(verb Verb) []params.Registration
|
||||
}
|
||||
|
||||
type GetHandler interface {
|
||||
Get(ctx context.Context, name string, params params.Container) (*ResourceList, error)
|
||||
}
|
||||
|
||||
type Verb string
|
||||
|
||||
const (
|
||||
VerbGet Verb = "get"
|
||||
)
|
||||
7
core/interfaces/http.go
Normal file
7
core/interfaces/http.go
Normal file
@ -0,0 +1,7 @@
|
||||
package interfaces
|
||||
|
||||
import "context"
|
||||
|
||||
type QueryClient[T any] interface {
|
||||
Get(ctx context.Context, queryStatement string, selectStatement string, allowCache bool) (*T, error)
|
||||
}
|
||||
14
core/interfaces/output.go
Normal file
14
core/interfaces/output.go
Normal file
@ -0,0 +1,14 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Formatter interface {
|
||||
Format(object *ResourceList) (io.Reader, error)
|
||||
}
|
||||
|
||||
type TableOutput interface {
|
||||
ColumnNames() []string
|
||||
Columns() []any
|
||||
}
|
||||
69
core/interfaces/params/params.go
Normal file
69
core/interfaces/params/params.go
Normal file
@ -0,0 +1,69 @@
|
||||
package params
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrParamNotFound = errors.New("parameter not found")
|
||||
)
|
||||
|
||||
type Registration struct {
|
||||
Name string
|
||||
ShortHand string
|
||||
Description string
|
||||
DefaultFunc func() string
|
||||
ParseFunc ParseFn
|
||||
}
|
||||
|
||||
type ParseFn func(value string) (any, error)
|
||||
|
||||
var ParseString ParseFn = func(value string) (any, error) { return value, nil }
|
||||
|
||||
type Container struct {
|
||||
params map[string]Value
|
||||
}
|
||||
|
||||
type Value struct {
|
||||
value *string
|
||||
parseFn ParseFn
|
||||
}
|
||||
|
||||
func (c *Container) Register(key string, parseFn ParseFn) *string {
|
||||
if c.params == nil {
|
||||
c.params = make(map[string]Value)
|
||||
}
|
||||
|
||||
item, exists := c.params[key]
|
||||
if exists {
|
||||
return item.value
|
||||
}
|
||||
|
||||
var str string
|
||||
value := Value{
|
||||
parseFn: parseFn,
|
||||
value: &str,
|
||||
}
|
||||
c.params[key] = value
|
||||
return value.value
|
||||
}
|
||||
|
||||
func (c *Container) GetValue(key string) (any, error) {
|
||||
item, exists := c.params[key]
|
||||
if !exists {
|
||||
return nil, ErrParamNotFound
|
||||
}
|
||||
|
||||
return item.parseFn(*item.value)
|
||||
}
|
||||
|
||||
// Returns a map of all parameters. Parameters that produce errors during parsing are ignored.
|
||||
func (c *Container) ToMap() (out map[string]any) {
|
||||
out = make(map[string]any)
|
||||
for key := range c.params {
|
||||
v, err := c.GetValue(key)
|
||||
if err == nil {
|
||||
out[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
12
core/interfaces/resource.go
Normal file
12
core/interfaces/resource.go
Normal file
@ -0,0 +1,12 @@
|
||||
package interfaces
|
||||
|
||||
type Resource interface {
|
||||
Kind() string
|
||||
|
||||
ItemName() string
|
||||
}
|
||||
|
||||
type ResourceList struct {
|
||||
ItemKind string
|
||||
Items []Resource
|
||||
}
|
||||
15
core/output/formatter.go
Normal file
15
core/output/formatter.go
Normal file
@ -0,0 +1,15 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
)
|
||||
|
||||
var Formatters = map[string]interfaces.Formatter{
|
||||
"json": &JsonFormatter{},
|
||||
"yaml": &YamlFormatter{},
|
||||
"go": &GoFormatter{},
|
||||
"table": &TableFormatter{},
|
||||
"csv": &TableFormatter{HideSummary: true, RenderFormat: tableFormatCSV},
|
||||
"html": &TableFormatter{HideSummary: true, RenderFormat: tableFormatHTML},
|
||||
"markdown": &TableFormatter{HideSummary: true, RenderFormat: tableFormatMarkdown},
|
||||
}
|
||||
15
core/output/go.go
Normal file
15
core/output/go.go
Normal file
@ -0,0 +1,15 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
)
|
||||
|
||||
type GoFormatter struct{}
|
||||
|
||||
func (f *GoFormatter) Format(list *interfaces.ResourceList) (io.Reader, error) {
|
||||
return strings.NewReader(fmt.Sprintf("%#v", list)), nil
|
||||
}
|
||||
17
core/output/json.go
Normal file
17
core/output/json.go
Normal file
@ -0,0 +1,17 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
)
|
||||
|
||||
type JsonFormatter struct{}
|
||||
|
||||
func (f *JsonFormatter) Format(list *interfaces.ResourceList) (io.Reader, error) {
|
||||
var buffer = make([]byte, 0, 1024)
|
||||
outputBuffer := bytes.NewBuffer(buffer)
|
||||
return outputBuffer, json.NewEncoder(outputBuffer).Encode(list)
|
||||
}
|
||||
81
core/output/table.go
Normal file
81
core/output/table.go
Normal file
@ -0,0 +1,81 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
"github.com/jedib0t/go-pretty/table"
|
||||
)
|
||||
|
||||
type tableRenderFormat string
|
||||
|
||||
const (
|
||||
tableFormatNormal tableRenderFormat = "table"
|
||||
tableFormatCSV tableRenderFormat = "csv"
|
||||
tableFormatHTML tableRenderFormat = "html"
|
||||
tableFormatMarkdown tableRenderFormat = "markdown"
|
||||
)
|
||||
|
||||
type TableFormatter struct {
|
||||
HideSummary bool
|
||||
RenderFormat tableRenderFormat
|
||||
}
|
||||
|
||||
func (f *TableFormatter) Format(list *interfaces.ResourceList) (io.Reader, error) {
|
||||
|
||||
var buffer = make([]byte, 0, 1024)
|
||||
outputBuffer := bytes.NewBuffer(buffer)
|
||||
|
||||
if !f.HideSummary {
|
||||
outputBuffer.WriteString(
|
||||
fmt.Sprintf("Resource: %s\r\nCount: %v\r\n\r\n", list.ItemKind, len(list.Items)),
|
||||
)
|
||||
}
|
||||
|
||||
if len(list.Items) <= 0 {
|
||||
return outputBuffer, nil
|
||||
}
|
||||
|
||||
// Setup table
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(outputBuffer)
|
||||
t.SetStyle(table.StyleLight)
|
||||
|
||||
// Write header
|
||||
tableFormat, ok := list.Items[0].(interfaces.TableOutput)
|
||||
headerRow := []any{"Name"}
|
||||
if ok {
|
||||
columnHeaders := util.Transform(tableFormat.ColumnNames(), func(i *string) any { return *i })
|
||||
headerRow = append(headerRow, columnHeaders...)
|
||||
}
|
||||
t.AppendHeader(headerRow)
|
||||
|
||||
// Write rows
|
||||
for _, item := range list.Items {
|
||||
rowOutput := []any{item.ItemName()}
|
||||
|
||||
itemFormatter, ok := item.(interfaces.TableOutput)
|
||||
if ok {
|
||||
rowOutput = append(rowOutput, itemFormatter.Columns()...)
|
||||
}
|
||||
|
||||
t.AppendRow(rowOutput)
|
||||
}
|
||||
|
||||
// Render
|
||||
switch f.RenderFormat {
|
||||
case tableFormatCSV:
|
||||
t.RenderCSV()
|
||||
case tableFormatHTML:
|
||||
t.RenderHTML()
|
||||
case tableFormatMarkdown:
|
||||
t.RenderMarkdown()
|
||||
default:
|
||||
t.Render()
|
||||
}
|
||||
|
||||
return outputBuffer, nil
|
||||
}
|
||||
20
core/output/yaml.go
Normal file
20
core/output/yaml.go
Normal file
@ -0,0 +1,20 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type YamlFormatter struct{}
|
||||
|
||||
func (f *YamlFormatter) Format(list *interfaces.ResourceList) (io.Reader, error) {
|
||||
buffer, err := yaml.Marshal(list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewBuffer(buffer), nil
|
||||
}
|
||||
23
core/services/jlog/context.go
Normal file
23
core/services/jlog/context.go
Normal file
@ -0,0 +1,23 @@
|
||||
package jlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const ctxkeyLogger ctxKey = "jlog:logger"
|
||||
|
||||
func FromContext(ctx context.Context) *slog.Logger {
|
||||
logger, exists := ctx.Value(ctxkeyLogger).(*slog.Logger)
|
||||
if !exists {
|
||||
return New(slog.LevelInfo)
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func ContextWith(ctx context.Context, logger *slog.Logger) context.Context {
|
||||
return context.WithValue(ctx, ctxkeyLogger, logger)
|
||||
}
|
||||
24
core/services/jlog/logger.go
Normal file
24
core/services/jlog/logger.go
Normal file
@ -0,0 +1,24 @@
|
||||
package jlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func New(level slog.Level) *slog.Logger {
|
||||
return NewFor(level, os.Stderr)
|
||||
}
|
||||
|
||||
func NewFor(level slog.Level, output io.Writer) *slog.Logger {
|
||||
return slog.New(slog.NewJSONHandler(output, &slog.HandlerOptions{
|
||||
// AddSource: true,
|
||||
Level: level,
|
||||
})).With(slog.String("traceid", newTraceID()))
|
||||
}
|
||||
|
||||
func newTraceID() string {
|
||||
return strconv.Itoa(rand.Intn(1000000))
|
||||
}
|
||||
66
core/services/stwhbclient/client.go
Normal file
66
core/services/stwhbclient/client.go
Normal file
@ -0,0 +1,66 @@
|
||||
package stwhbclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/external/stwbremen"
|
||||
)
|
||||
|
||||
type stwHBClientFactory struct{}
|
||||
|
||||
func NewFactory() *stwHBClientFactory { return &stwHBClientFactory{} }
|
||||
|
||||
func Create[T any]() *stwHBClient[T] {
|
||||
return New[T]()
|
||||
}
|
||||
|
||||
type stwHBClient[T any] struct {
|
||||
interfaces.QueryClient[T]
|
||||
|
||||
client *http.Client
|
||||
baseUrl string
|
||||
}
|
||||
|
||||
func New[T any]() *stwHBClient[T] {
|
||||
return &stwHBClient[T]{
|
||||
client: http.DefaultClient,
|
||||
baseUrl: "https://content.stw-bremen.de",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *stwHBClient[T]) Get(ctx context.Context, queryStatement string, selectStatement string, allowCache bool) (*T, error) {
|
||||
// Setup url
|
||||
url := c.baseUrl + "/api/kql"
|
||||
if !allowCache {
|
||||
url = url + "nocache"
|
||||
}
|
||||
|
||||
// Create request from query
|
||||
fullQuery := fmt.Sprintf(`{"query": "%s", "select": %s}`, queryStatement, selectStatement)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(fullQuery))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
// Perform request
|
||||
response, err := c.client.Do(req)
|
||||
if err != nil || response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("performing request failed: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Read response
|
||||
var result stwbremen.Result[T]
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse result: %w", err)
|
||||
}
|
||||
|
||||
return &result.Result, nil
|
||||
|
||||
}
|
||||
25
go.mod
Normal file
25
go.mod
Normal file
@ -0,0 +1,25 @@
|
||||
module git.bissendorf.co/bissendorf/unifood/m/v2
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible
|
||||
github.com/spf13/cobra v1.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
)
|
||||
45
go.sum
Normal file
45
go.sum
Normal file
@ -0,0 +1,45 @@
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
||||
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
|
||||
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
7
main.go
Normal file
7
main.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "git.bissendorf.co/bissendorf/unifood/m/v2/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
22
model/external/stwbremen/meal.go
vendored
Normal file
22
model/external/stwbremen/meal.go
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
package stwbremen
|
||||
|
||||
type Meal struct {
|
||||
Title string `json:"title"`
|
||||
ID string `json:"id"`
|
||||
Ingredients []Ingredient `json:"ingredients"`
|
||||
Prices []Price `json:"prices"`
|
||||
Location string `json:"location"`
|
||||
Date string `json:"date"` // YYYY-MM-DD
|
||||
Counter string `json:"counter"`
|
||||
Tags string `json:"mealadds"`
|
||||
}
|
||||
|
||||
type Ingredient struct {
|
||||
Label string `json:"label"`
|
||||
Additionals []string `json:"additionals"`
|
||||
}
|
||||
|
||||
type Price struct {
|
||||
Label string `json:"label"`
|
||||
Price string `json:"price"`
|
||||
}
|
||||
35
model/external/stwbremen/restaurant.go
vendored
Normal file
35
model/external/stwbremen/restaurant.go
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
package stwbremen
|
||||
|
||||
type RestaurantList struct {
|
||||
Items []string `json:"items"`
|
||||
}
|
||||
|
||||
type Restaurant struct {
|
||||
Title string `json:"title"`
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
Address string `json:"address"`
|
||||
|
||||
OpeningHours
|
||||
}
|
||||
|
||||
type OpeningHours struct {
|
||||
OpeningTimes []OpeningTime `json:"openingTimes"`
|
||||
OffseasonOpeningTimes []OpeningTime `json:"offseasonOpeningTimes"`
|
||||
OffseasonStart DateOnly `json:"offseasonStart"`
|
||||
OffseasonEnd DateOnly `json:"offseasonEnd"`
|
||||
ChangedTimes []ChangedTime `json:"changedTimes"`
|
||||
}
|
||||
|
||||
type OpeningTime struct {
|
||||
Weekday string `json:"weekday"`
|
||||
OpeningTime TimeOnly `json:"openingTime"`
|
||||
ClosingTime TimeOnly `json:"closingTime"`
|
||||
}
|
||||
|
||||
type ChangedTime struct {
|
||||
StartDate DateOnly `json:"startDate"`
|
||||
EndDate DateOnly `json:"endDate"`
|
||||
OpeningTime TimeOnly `json:"openingTime"`
|
||||
ClosingTime TimeOnly `json:"closingTime"`
|
||||
}
|
||||
7
model/external/stwbremen/result.go
vendored
Normal file
7
model/external/stwbremen/result.go
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package stwbremen
|
||||
|
||||
type Result[T any] struct {
|
||||
Code uint16 `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Result T `json:"result"`
|
||||
}
|
||||
53
model/external/stwbremen/types.go
vendored
Normal file
53
model/external/stwbremen/types.go
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
package stwbremen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeOnly struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a JSON string in HH:MM:SS format into a TimeOnly
|
||||
func (t *TimeOnly) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, err := time.Parse(time.TimeOnly, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Time = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON converts the TimeOnly to a JSON string in HH:MM:SS format
|
||||
func (t TimeOnly) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.Format(time.TimeOnly))
|
||||
}
|
||||
|
||||
type DateOnly struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a JSON string in YYYY-MM-DD format into a DateOnly
|
||||
func (d *DateOnly) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, err := time.Parse(time.DateOnly, s)
|
||||
if err != nil {
|
||||
d.Time = time.Time{}
|
||||
} else {
|
||||
d.Time = parsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON converts the DateOnly to a JSON string in YYYY-MM-DD format
|
||||
func (d DateOnly) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(d.Format(time.DateOnly))
|
||||
}
|
||||
71
model/resources/meals.go
Normal file
71
model/resources/meals.go
Normal file
@ -0,0 +1,71 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/model/external/stwbremen"
|
||||
"git.bissendorf.co/bissendorf/unifood/m/v2/util"
|
||||
)
|
||||
|
||||
func MealFromDTO(meal stwbremen.Meal) (*Meal, error) {
|
||||
date, err := time.Parse(time.DateOnly, meal.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse meal date: %w", err)
|
||||
}
|
||||
|
||||
id, _ := strings.CutPrefix(meal.ID, "meals/")
|
||||
|
||||
return &Meal{
|
||||
Title: meal.Title,
|
||||
RestaurantID: meal.Location,
|
||||
ID: id,
|
||||
Date: date,
|
||||
Tags: strings.Split(strings.Replace(meal.Tags, " ", "", -1), ","),
|
||||
Counter: meal.Counter,
|
||||
Prices: util.Map(meal.Prices, func(i *stwbremen.Price) (string, float32) {
|
||||
p, err := strconv.ParseFloat(strings.Trim(i.Price, " "), 32)
|
||||
if err != nil {
|
||||
p = 0
|
||||
}
|
||||
return i.Label, float32(p)
|
||||
}),
|
||||
Ingredients: util.Select(util.Transform(meal.Ingredients, func(i *stwbremen.Ingredient) ingredient {
|
||||
return ingredient{
|
||||
Name: i.Label,
|
||||
Additionals: i.Additionals,
|
||||
}
|
||||
}), func(i *ingredient) bool { return i.Name != "" }),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const ResourceMeal = "meal"
|
||||
|
||||
type Meal struct {
|
||||
Title string
|
||||
ID string
|
||||
RestaurantID string
|
||||
Ingredients []ingredient
|
||||
Prices map[string]float32
|
||||
Date time.Time
|
||||
Counter string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type ingredient struct {
|
||||
Name string
|
||||
Additionals []string
|
||||
}
|
||||
|
||||
func (d *Meal) Kind() string { return ResourceMeal }
|
||||
func (d *Meal) ItemName() string { return d.ID }
|
||||
|
||||
// Table output
|
||||
func (d *Meal) ColumnNames() []string {
|
||||
return []string{"Date", "Restaurant", "Title", "Counter", "Price"}
|
||||
}
|
||||
func (d *Meal) Columns() []any {
|
||||
return []any{d.Date.Format(time.DateOnly), d.RestaurantID, d.Title, d.Counter, d.Prices["Studierende"]}
|
||||
}
|
||||
20
model/resources/resources.go
Normal file
20
model/resources/resources.go
Normal file
@ -0,0 +1,20 @@
|
||||
package resources
|
||||
|
||||
import "strings"
|
||||
|
||||
const ResourceResource = "resource"
|
||||
|
||||
type Resource struct {
|
||||
ResourceName string
|
||||
Aliases []string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (r *Resource) Kind() string { return ResourceResource }
|
||||
func (r *Resource) ItemName() string { return r.ResourceName }
|
||||
|
||||
// Table output
|
||||
func (r *Resource) ColumnNames() []string { return []string{"Aliases", "Description"} }
|
||||
func (r *Resource) Columns() []any {
|
||||
return []any{strings.Join(r.Aliases, ", "), r.Description}
|
||||
}
|
||||
21
model/resources/restaurant.go
Normal file
21
model/resources/restaurant.go
Normal file
@ -0,0 +1,21 @@
|
||||
package resources
|
||||
|
||||
const ResourceRestaurant = "restaurant"
|
||||
|
||||
type Restaurant struct {
|
||||
Name string
|
||||
Title string
|
||||
ID string
|
||||
Address string
|
||||
Image string
|
||||
OpeningHours any
|
||||
}
|
||||
|
||||
func (r *Restaurant) Kind() string { return ResourceRestaurant }
|
||||
func (r *Restaurant) ItemName() string { return r.Name }
|
||||
|
||||
// Table output
|
||||
func (r *Restaurant) ColumnNames() []string { return []string{"ID", "Title"} }
|
||||
func (r *Restaurant) Columns() []any {
|
||||
return []any{r.ID, r.Title}
|
||||
}
|
||||
19
model/resources/university.go
Normal file
19
model/resources/university.go
Normal file
@ -0,0 +1,19 @@
|
||||
package resources
|
||||
|
||||
import "strings"
|
||||
|
||||
const ResourceUniversity = "university"
|
||||
|
||||
type University struct {
|
||||
Name string
|
||||
Restaurants []string
|
||||
}
|
||||
|
||||
func (u *University) Kind() string { return ResourceUniversity }
|
||||
func (u *University) ItemName() string { return u.Name }
|
||||
|
||||
// Table output
|
||||
func (u *University) ColumnNames() []string { return []string{"Restaurants"} }
|
||||
func (u *University) Columns() []any {
|
||||
return []any{strings.Join(u.Restaurants, "\r\n")}
|
||||
}
|
||||
48
util/slices.go
Normal file
48
util/slices.go
Normal file
@ -0,0 +1,48 @@
|
||||
package util
|
||||
|
||||
func Transform[Tin any, Tout any](s []Tin, transformFn func(i *Tin) Tout) (out []Tout) {
|
||||
out = make([]Tout, 0)
|
||||
|
||||
for _, item := range s {
|
||||
out = append(out, transformFn(&item))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Select[T any](s []T, selectFn func(i *T) bool) (out []T) {
|
||||
out = make([]T, 0)
|
||||
|
||||
for _, item := range s {
|
||||
if selectFn(&item) {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Map[T any, Tkey comparable, Tval any](s []T, transformFn func(i *T) (Tkey, Tval)) (out map[Tkey]Tval) {
|
||||
out = make(map[Tkey]Tval)
|
||||
for _, i := range s {
|
||||
key, val := transformFn(&i)
|
||||
out[key] = val
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Group[T any, Tkey comparable, Tval any](s []T, groupFn func(i *T) (groupKey Tkey, value Tval)) (out map[Tkey][]Tval) {
|
||||
out = make(map[Tkey][]Tval)
|
||||
for _, i := range s {
|
||||
key, value := groupFn(&i)
|
||||
list, exists := out[key]
|
||||
if !exists {
|
||||
list = make([]Tval, 0, 2)
|
||||
}
|
||||
|
||||
out[key] = append(list, value)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
3
util/version.go
Normal file
3
util/version.go
Normal file
@ -0,0 +1,3 @@
|
||||
package util
|
||||
|
||||
const AppVersion = "v0.1"
|
||||
Reference in New Issue
Block a user