feat: base implementation

This commit is contained in:
2025-07-20 13:47:16 +02:00
parent ad082a3f12
commit 7d561ea6ea
21 changed files with 635 additions and 0 deletions

88
core/handler/menu/menu.go Normal file
View File

@ -0,0 +1,88 @@
package menu
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 MenuHandler struct {
interfaces.ResourceHandler
interfaces.GetHandler
QueryClient interfaces.QueryClient[[]stwbremen.Dish]
}
const (
paramDate = "date"
paramLocation = "location"
)
func (h *MenuHandler) Get(ctx context.Context, params params.Container) (any, 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 &resources.Menu{
Location: location.(string),
Dishes: util.Transform(*dishes, func(i *stwbremen.Dish) resources.Dish {
d, err := resources.DishFromDTO(*i)
if err != nil {
return resources.Dish{}
}
return *d
}),
}, nil
}
func (h *MenuHandler) 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,
},
}
}

View 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, params params.Container) (any, error)
}
type Verb string
const (
VerbGet Verb = "get"
)

7
core/interfaces/http.go Normal file
View 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)
}

View File

@ -0,0 +1,56 @@
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)
}

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

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

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