feat: unifood base with GET dishes #1

Merged
bissendorf merged 6 commits from feat/base into dev 2025-07-20 17:29:04 +00:00
27 changed files with 893 additions and 0 deletions
Showing only changes of commit 4a15a4db66 - Show all commits

2
.vscode/launch.json vendored
View File

@ -13,8 +13,6 @@
"args": [
"get", "dishes",
"-o", "yaml",
"-d", "2025-07-21",
"--print-config"
]
}
]

View File

@ -31,7 +31,7 @@ func initRootCmd() {
var appConfig AppConfig
rootCmd.PersistentFlags().BoolVarP(&appConfig.OutputVerbose, "verbose", "v", false, "Enable verbose output")
rootCmd.PersistentFlags().StringVarP(&appConfig.OutputFormatter, "output", "o", "json", "Set output format")
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)

View File

@ -26,7 +26,7 @@ type VerbItem struct {
var verbs = []VerbItem{
{
Name: interfaces.VerbGet,
Description: "Retrieve resource information",
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 {

View File

@ -24,7 +24,7 @@ const (
paramLocation = "location"
)
func (h *DishesHandler) Get(ctx context.Context, params params.Container) ([]any, error) {
func (h *DishesHandler) Get(ctx context.Context, params params.Container) (*interfaces.ResourceList, error) {
// Read parameters
p, err := params.GetValue(paramDate)
if err != nil {
@ -55,14 +55,17 @@ func (h *DishesHandler) Get(ctx context.Context, params params.Container) ([]any
}
// Return
return util.Transform(*dishes, func(i *stwbremen.Dish) any {
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 &resources.Dish{}
}
return *d
}), nil
return d
}),
}, nil
}

View File

@ -19,7 +19,7 @@ type ResourceHandler interface {
}
type GetHandler interface {
Get(ctx context.Context, params params.Container) ([]any, error)
Get(ctx context.Context, params params.Container) (*ResourceList, error)
}
type Verb string

View File

@ -1,7 +1,14 @@
package interfaces
import "io"
import (
"io"
)
type Formatter interface {
Format(object []any) (io.Reader, error)
Format(object *ResourceList) (io.Reader, error)
}
type TableOutput interface {
ColumnNames() []string
Columns() []any
}

View File

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

View File

@ -8,5 +8,8 @@ var Formatters = map[string]interfaces.Formatter{
"json": &JsonFormatter{},
"yaml": &YamlFormatter{},
"go": &GoFormatter{},
"table": nil,
"table": &TableFormatter{},
"csv": &TableFormatter{HideSummary: true, RenderFormat: tableFormatCSV},
"html": &TableFormatter{HideSummary: true, RenderFormat: tableFormatHTML},
"markdown": &TableFormatter{HideSummary: true, RenderFormat: tableFormatMarkdown},
}

View File

@ -4,10 +4,12 @@ import (
"fmt"
"io"
"strings"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
)
type GoFormatter struct{}
func (f *GoFormatter) Format(objects []any) (io.Reader, error) {
return strings.NewReader(fmt.Sprintf("%#v", objects)), nil
func (f *GoFormatter) Format(list *interfaces.ResourceList) (io.Reader, error) {
return strings.NewReader(fmt.Sprintf("%#v", list)), nil
}

View File

@ -4,12 +4,14 @@ import (
"bytes"
"encoding/json"
"io"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
)
type JsonFormatter struct{}
func (f *JsonFormatter) Format(objects []any) (io.Reader, error) {
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(objects)
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
}

View File

@ -4,13 +4,14 @@ import (
"bytes"
"io"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/interfaces"
"github.com/goccy/go-yaml"
)
type YamlFormatter struct{}
func (f *YamlFormatter) Format(objects []any) (io.Reader, error) {
buffer, err := yaml.Marshal(objects)
func (f *YamlFormatter) Format(list *interfaces.ResourceList) (io.Reader, error) {
buffer, err := yaml.Marshal(list)
if err != nil {
return nil, err
}

18
go.mod
View File

@ -2,10 +2,24 @@ module git.bissendorf.co/bissendorf/unifood/m/v2
go 1.24.0
require github.com/spf13/cobra v1.9.1
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/goccy/go-yaml v1.18.0 // indirect
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
)

32
go.sum
View File

@ -1,13 +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=

View File

@ -18,18 +18,16 @@ func DishFromDTO(dish stwbremen.Dish) (*Dish, error) {
return &Dish{
Title: dish.Title,
Location: dish.Location,
Date: date,
Tags: strings.Split(strings.Replace(dish.Tags, " ", "", -1), ","),
Counter: dish.Counter,
Prices: util.Transform(dish.Prices, func(i *stwbremen.Price) price {
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 price{
Label: i.Label,
Price: float32(p),
}
return i.Label, float32(p)
}),
Ingredients: util.Select(util.Transform(dish.Ingredients, func(i *stwbremen.Ingredient) ingredient {
return ingredient{
@ -40,10 +38,13 @@ func DishFromDTO(dish stwbremen.Dish) (*Dish, error) {
}, nil
}
const ResourceDish = "dish"
type Dish struct {
Title string
Location string
Ingredients []ingredient
Prices []price
Prices map[string]float32
Date time.Time
Counter string
Tags []string
@ -54,7 +55,10 @@ type ingredient struct {
Additionals []string
}
type price struct {
Label string
Price float32
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"]}
}

View File

@ -21,3 +21,13 @@ func Select[T any](s []T, selectFn func(i *T) bool) (out []T) {
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
}