feat: unifood base with GET dishes (#1)

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

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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
unifood
__bin*

19
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
// 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", "dishes",
"-o", "yaml",
]
}
]
}

7
cmd/config.go Normal file
View File

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

19
cmd/resources.go Normal file
View File

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

41
cmd/root.go Normal file
View File

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

122
cmd/verbs.go Normal file
View File

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

View File

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

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) (*ResourceList, 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)
}

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

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

View File

@ -0,0 +1,12 @@
package interfaces
type Resource interface {
Kind() string
Name() string
}
type ResourceList struct {
ItemKind string
Items []Resource
}

15
core/output/formatter.go Normal file
View 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
View 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
View 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
View 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.Name()}
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
View 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
}

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
}

25
go.mod Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
package main
import "git.bissendorf.co/bissendorf/unifood/m/v2/cmd"
func main() {
cmd.Execute()
}

21
model/external/stwbremen/dish.go vendored Normal file
View File

@ -0,0 +1,21 @@
package stwbremen
type Dish struct {
Title string `json:"title"`
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"`
}

7
model/external/stwbremen/result.go vendored Normal file
View File

@ -0,0 +1,7 @@
package stwbremen
type Result[T any] struct {
Code uint16 `json:"code"`
Status string `json:"status"`
Result T `json:"result"`
}

64
model/resources/dish.go Normal file
View File

@ -0,0 +1,64 @@
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"]}
}

33
util/slices.go Normal file
View File

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