Compare commits
4 Commits
975fb67520
...
v0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 723aae48ce | |||
| 90a5125c66 | |||
| 4b7866da03 | |||
| e395b0b4ca |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
unifood
|
unifood
|
||||||
__bin*
|
__bin*
|
||||||
|
__debug*
|
||||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -11,8 +11,7 @@
|
|||||||
"mode": "debug",
|
"mode": "debug",
|
||||||
"program": "${workspaceFolder}/main.go",
|
"program": "${workspaceFolder}/main.go",
|
||||||
"args": [
|
"args": [
|
||||||
"get", "dishes",
|
"get", "meals",
|
||||||
"-o", "yaml",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
OutputVerbose bool
|
OutputVerbose bool
|
||||||
OutputFormatter string
|
OutputFormatter string
|
||||||
PrintConfig bool
|
OutputOrderReverse bool
|
||||||
|
PrintConfig bool
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,11 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/handler/dishes"
|
"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"
|
||||||
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces/params"
|
"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/core/services/stwhbclient"
|
||||||
@ -15,28 +18,56 @@ import (
|
|||||||
var availableResources = []interfaces.ResourceCommand[any]{
|
var availableResources = []interfaces.ResourceCommand[any]{
|
||||||
{
|
{
|
||||||
Name: "resources",
|
Name: "resources",
|
||||||
Aliases: []string{"resource", "r"},
|
Aliases: []string{"resource"},
|
||||||
Description: "A meta representation of a usable resources of this CLI",
|
Description: "A meta resource representing all other object kinds of this CLI.",
|
||||||
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
||||||
Handler: ®isteredResourcesHandler{},
|
Handler: ®isteredResourcesHandler{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "dishes",
|
Name: "meals",
|
||||||
Aliases: []string{"dish", "d"},
|
Aliases: []string{"meal", "m"},
|
||||||
Description: "A dish represents a cooked combination of ingredients that can be bought and consumed",
|
Description: "A meal represents a cooked combination of ingredients that can be bought and consumed.",
|
||||||
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
Verbs: []interfaces.Verb{interfaces.VerbGet},
|
||||||
Handler: &dishes.DishesHandler{
|
Handler: &meals.MealsHandler{
|
||||||
QueryClient: stwhbclient.New[[]stwbremen.Dish](),
|
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{}
|
type registeredResourcesHandler struct{}
|
||||||
|
|
||||||
func (h *registeredResourcesHandler) Get(ctx context.Context, params params.Container) (*interfaces.ResourceList, error) {
|
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{
|
return &interfaces.ResourceList{
|
||||||
ItemKind: "",
|
ItemKind: resources.ResourceResource,
|
||||||
Items: util.Transform(availableResources, func(i *interfaces.ResourceCommand[any]) interfaces.Resource {
|
Items: util.Transform(list, func(i *interfaces.ResourceCommand[any]) interfaces.Resource {
|
||||||
return &resources.Resource{
|
return &resources.Resource{
|
||||||
ResourceName: i.Name,
|
ResourceName: i.Name,
|
||||||
Aliases: i.Aliases,
|
Aliases: i.Aliases,
|
||||||
|
|||||||
@ -32,10 +32,12 @@ func initRootCmd() {
|
|||||||
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&appConfig.OutputVerbose, "verbose", "v", false, "Enable verbose output")
|
rootCmd.PersistentFlags().BoolVarP(&appConfig.OutputVerbose, "verbose", "v", false, "Enable verbose output")
|
||||||
rootCmd.PersistentFlags().StringVarP(&appConfig.OutputFormatter, "output", "o", "table", "Set output format")
|
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")
|
rootCmd.PersistentFlags().BoolVar(&appConfig.PrintConfig, "print-config", false, "Enable printing the application config")
|
||||||
|
|
||||||
logger := jlog.New(slog.LevelDebug)
|
logger := jlog.New(slog.LevelDebug)
|
||||||
ctx := jlog.ContextWith(context.Background(), logger)
|
ctx := jlog.ContextWith(context.Background(), logger)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(&versionCmd)
|
||||||
rootCmd.AddCommand(getVerbs(ctx, &appConfig)...)
|
rootCmd.AddCommand(getVerbs(ctx, &appConfig)...)
|
||||||
}
|
}
|
||||||
|
|||||||
18
cmd/verbs.go
18
cmd/verbs.go
@ -20,24 +20,27 @@ type VerbItem struct {
|
|||||||
Name interfaces.Verb
|
Name interfaces.Verb
|
||||||
Aliases []string
|
Aliases []string
|
||||||
Description string
|
Description string
|
||||||
RunFn func(ctx context.Context, config *AppConfig, handler interfaces.ResourceHandler, params params.Container) error
|
RunFn func(ctx context.Context, config *AppConfig, handler interfaces.ResourceHandler, params params.Container, name string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var verbs = []VerbItem{
|
var verbs = []VerbItem{
|
||||||
{
|
{
|
||||||
Name: interfaces.VerbGet,
|
Name: interfaces.VerbGet,
|
||||||
Description: "Retrieve a list of resources",
|
Description: "Retrieve a list of resources",
|
||||||
RunFn: func(ctx context.Context, config *AppConfig, handler interfaces.ResourceHandler, params params.Container) error {
|
RunFn: func(ctx context.Context, config *AppConfig, handler interfaces.ResourceHandler, params params.Container, name string) error {
|
||||||
h, ok := handler.(interfaces.GetHandler)
|
h, ok := handler.(interfaces.GetHandler)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("resource does not support GET")
|
return fmt.Errorf("resource does not support GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get items
|
// Get items
|
||||||
items, err := h.Get(ctx, params)
|
items, err := h.Get(ctx, name, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("retrieving item failed: %w", err)
|
return fmt.Errorf("retrieving item failed: %w", err)
|
||||||
}
|
}
|
||||||
|
if config.OutputOrderReverse {
|
||||||
|
slices.Reverse(items.Items)
|
||||||
|
}
|
||||||
|
|
||||||
formatterName := strings.ToLower(config.OutputFormatter)
|
formatterName := strings.ToLower(config.OutputFormatter)
|
||||||
formatter, exists := output.Formatters[formatterName]
|
formatter, exists := output.Formatters[formatterName]
|
||||||
@ -80,6 +83,7 @@ func getVerbs(ctx context.Context, config *AppConfig) (commands []*cobra.Command
|
|||||||
Use: r.Name,
|
Use: r.Name,
|
||||||
Aliases: r.Aliases,
|
Aliases: r.Aliases,
|
||||||
Short: r.Description,
|
Short: r.Description,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Configure log
|
// Configure log
|
||||||
var logLevel = slog.LevelWarn
|
var logLevel = slog.LevelWarn
|
||||||
@ -94,7 +98,12 @@ func getVerbs(ctx context.Context, config *AppConfig) (commands []*cobra.Command
|
|||||||
logger.WarnContext(ctx, "Printing app config", slog.Any("config", config), slog.Any("parameters", params.ToMap()))
|
logger.WarnContext(ctx, "Printing app config", slog.Any("config", config), slog.Any("parameters", params.ToMap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := v.RunFn(ctx, config, r.Handler, params)
|
var name string
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
err := v.RunFn(ctx, config, r.Handler, params, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, fmt.Sprintf("%s %s failed", strings.ToUpper(string(v.Name)), r.Name), "error", err.Error())
|
logger.ErrorContext(ctx, fmt.Sprintf("%s %s failed", strings.ToUpper(string(v.Name)), r.Name), "error", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -112,6 +121,7 @@ func getVerbs(ctx context.Context, config *AppConfig) (commands []*cobra.Command
|
|||||||
param.Description,
|
param.Description,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
verbCommand.AddCommand(resourceCommand)
|
verbCommand.AddCommand(resourceCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,89 +0,0 @@
|
|||||||
package dishes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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 DishesHandler struct {
|
|
||||||
interfaces.ResourceHandler
|
|
||||||
interfaces.GetHandler
|
|
||||||
|
|
||||||
QueryClient interfaces.QueryClient[[]stwbremen.Dish]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
paramDate = "date"
|
|
||||||
paramLocation = "location"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *DishesHandler) Get(ctx context.Context, params params.Container) (*interfaces.ResourceList, error) {
|
|
||||||
// Read parameters
|
|
||||||
p, err := params.GetValue(paramDate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse date parameter: %w", err)
|
|
||||||
}
|
|
||||||
date := p.(time.Time)
|
|
||||||
|
|
||||||
location, err := params.GetValue(paramLocation)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse location parameter: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build query
|
|
||||||
query := fmt.Sprintf(
|
|
||||||
`page('meals').children.filterBy('location', '%s').filterBy('date', '%s').filterBy('printonly', 0)`,
|
|
||||||
location,
|
|
||||||
date.Format(time.DateOnly),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run query
|
|
||||||
dishes, err := h.QueryClient.Get(ctx,
|
|
||||||
query,
|
|
||||||
`{"title":true,"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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
return &interfaces.ResourceList{
|
|
||||||
ItemKind: resources.ResourceDish,
|
|
||||||
Items: util.Transform(*dishes, func(i *stwbremen.Dish) interfaces.Resource {
|
|
||||||
d, err := resources.DishFromDTO(*i)
|
|
||||||
if err != nil {
|
|
||||||
return &resources.Dish{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}),
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DishesHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
|
|
||||||
return []params.Registration{
|
|
||||||
{
|
|
||||||
Name: paramDate,
|
|
||||||
ShortHand: "d",
|
|
||||||
Description: "Menu date",
|
|
||||||
DefaultFunc: func() string { return time.Now().Format(time.DateOnly) },
|
|
||||||
ParseFunc: func(value string) (any, error) { return time.Parse(time.DateOnly, value) },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: paramLocation,
|
|
||||||
ShortHand: "l",
|
|
||||||
Description: "Define the restaurant",
|
|
||||||
DefaultFunc: func() string { return "330" },
|
|
||||||
ParseFunc: params.ParseString,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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{}
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ type ResourceHandler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetHandler interface {
|
type GetHandler interface {
|
||||||
Get(ctx context.Context, params params.Container) (*ResourceList, error)
|
Get(ctx context.Context, name string, params params.Container) (*ResourceList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Verb string
|
type Verb string
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package interfaces
|
|||||||
type Resource interface {
|
type Resource interface {
|
||||||
Kind() string
|
Kind() string
|
||||||
|
|
||||||
Name() string
|
ItemName() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceList struct {
|
type ResourceList struct {
|
||||||
|
|||||||
@ -55,7 +55,7 @@ func (f *TableFormatter) Format(list *interfaces.ResourceList) (io.Reader, error
|
|||||||
|
|
||||||
// Write rows
|
// Write rows
|
||||||
for _, item := range list.Items {
|
for _, item := range list.Items {
|
||||||
rowOutput := []any{item.Name()}
|
rowOutput := []any{item.ItemName()}
|
||||||
|
|
||||||
itemFormatter, ok := item.(interfaces.TableOutput)
|
itemFormatter, ok := item.(interfaces.TableOutput)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
package stwbremen
|
package stwbremen
|
||||||
|
|
||||||
type Dish struct {
|
type Meal struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
ID string `json:"id"`
|
||||||
Ingredients []Ingredient `json:"ingredients"`
|
Ingredients []Ingredient `json:"ingredients"`
|
||||||
Prices []Price `json:"prices"`
|
Prices []Price `json:"prices"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
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"`
|
||||||
|
}
|
||||||
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))
|
||||||
|
}
|
||||||
@ -1,64 +0,0 @@
|
|||||||
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 DishFromDTO(dish stwbremen.Dish) (*Dish, error) {
|
|
||||||
date, err := time.Parse(time.DateOnly, dish.Date)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse dish date: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Dish{
|
|
||||||
Title: dish.Title,
|
|
||||||
Location: dish.Location,
|
|
||||||
Date: date,
|
|
||||||
Tags: strings.Split(strings.Replace(dish.Tags, " ", "", -1), ","),
|
|
||||||
Counter: dish.Counter,
|
|
||||||
Prices: util.Map(dish.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(dish.Ingredients, func(i *stwbremen.Ingredient) ingredient {
|
|
||||||
return ingredient{
|
|
||||||
Name: i.Label,
|
|
||||||
Additionals: i.Additionals,
|
|
||||||
}
|
|
||||||
}), func(i *ingredient) bool { return i.Name != "" }),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResourceDish = "dish"
|
|
||||||
|
|
||||||
type Dish struct {
|
|
||||||
Title string
|
|
||||||
Location string
|
|
||||||
Ingredients []ingredient
|
|
||||||
Prices map[string]float32
|
|
||||||
Date time.Time
|
|
||||||
Counter string
|
|
||||||
Tags []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ingredient struct {
|
|
||||||
Name string
|
|
||||||
Additionals []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dish) Kind() string { return ResourceDish }
|
|
||||||
func (d *Dish) Name() string { return d.Title }
|
|
||||||
|
|
||||||
func (d *Dish) ColumnNames() []string { return []string{"Location", "Date", "Counter", "Price"} }
|
|
||||||
func (d *Dish) Columns() []any {
|
|
||||||
return []any{d.Location, d.Date.Format(time.DateOnly), d.Counter, d.Prices["Studierende"]}
|
|
||||||
}
|
|
||||||
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"]}
|
||||||
|
}
|
||||||
@ -2,15 +2,18 @@ package resources
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
|
const ResourceResource = "resource"
|
||||||
|
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
ResourceName string
|
ResourceName string
|
||||||
Aliases []string
|
Aliases []string
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) Kind() string { return "Resource" }
|
func (r *Resource) Kind() string { return ResourceResource }
|
||||||
func (r *Resource) Name() string { return r.ResourceName }
|
func (r *Resource) ItemName() string { return r.ResourceName }
|
||||||
|
|
||||||
|
// Table output
|
||||||
func (r *Resource) ColumnNames() []string { return []string{"Aliases", "Description"} }
|
func (r *Resource) ColumnNames() []string { return []string{"Aliases", "Description"} }
|
||||||
func (r *Resource) Columns() []any {
|
func (r *Resource) Columns() []any {
|
||||||
return []any{strings.Join(r.Aliases, ", "), r.Description}
|
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")}
|
||||||
|
}
|
||||||
@ -31,3 +31,18 @@ func Map[T any, Tkey comparable, Tval any](s []T, transformFn func(i *T) (Tkey,
|
|||||||
|
|
||||||
return
|
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