feat: add universities and restaurants

This commit is contained in:
2025-07-21 00:10:15 +02:00
parent e395b0b4ca
commit eb7c849552
20 changed files with 408 additions and 33 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt"
"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"
@ -19,20 +20,15 @@ type MealsHandler struct {
QueryClient interfaces.QueryClient[[]stwbremen.Meal]
}
const (
paramDate = "date"
paramLocation = "location"
)
func (h *MealsHandler) Get(ctx context.Context, params params.Container) (*interfaces.ResourceList, error) {
func (h *MealsHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) {
// Read parameters
p, err := params.GetValue(paramDate)
p, err := params.GetValue(handler.ParamDate)
if err != nil {
return nil, fmt.Errorf("unable to parse date parameter: %w", err)
}
date := p.(time.Time)
location, err := params.GetValue(paramLocation)
location, err := params.GetValue(handler.ParamLocation)
if err != nil {
return nil, fmt.Errorf("unable to parse location parameter: %w", err)
}
@ -72,16 +68,16 @@ func (h *MealsHandler) Get(ctx context.Context, params params.Container) (*inter
func (h *MealsHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
return []params.Registration{
{
Name: paramDate,
Name: handler.ParamDate,
ShortHand: "d",
Description: "Menu date",
Description: "Meal date",
DefaultFunc: func() string { return time.Now().Format(time.DateOnly) },
ParseFunc: func(value string) (any, error) { return time.Parse(time.DateOnly, value) },
},
{
Name: paramLocation,
Name: handler.ParamLocation,
ShortHand: "l",
Description: "Define the restaurant",
Description: "Select a restaurant",
DefaultFunc: func() string { return "330" },
ParseFunc: params.ParseString,
},

6
core/handler/params.go Normal file
View File

@ -0,0 +1,6 @@
package handler
const (
ParamDate string = "date"
ParamLocation string = "location"
)

View 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{}
}

View 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
}

View 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{}
}