1
0
Fork 0

feat!: add slug to training

BREAKING CHANGE: update init migration
This commit is contained in:
Vojtěch Mareš 2024-06-26 22:24:36 +02:00
parent 556b4f4e79
commit 2d32c80182
Signed by: vojtech.mares
GPG key ID: C6827B976F17240D
12 changed files with 219 additions and 45 deletions

View file

@ -529,6 +529,8 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/TrainingPrice" $ref: "#/components/schemas/TrainingPrice"
slug:
type: string
required: required:
- name - name
- days - days
@ -544,6 +546,7 @@ components:
$ref: "#/components/schemas/TrainingID" $ref: "#/components/schemas/TrainingID"
required: required:
- id - id
- slug
TrainingID: TrainingID:
type: integer type: integer

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/gofiber/contrib/fiberzap/v2 v2.1.3 github.com/gofiber/contrib/fiberzap/v2 v2.1.3
github.com/gofiber/contrib/swagger v1.1.2 github.com/gofiber/contrib/swagger v1.1.2
github.com/gofiber/fiber/v2 v2.52.4 github.com/gofiber/fiber/v2 v2.52.4
github.com/gosimple/unidecode v1.0.1
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
github.com/jackc/pgx/v5 v5.6.0 github.com/jackc/pgx/v5 v5.6.0
github.com/oapi-codegen/fiber-middleware v1.0.2 github.com/oapi-codegen/fiber-middleware v1.0.2

2
go.sum
View file

@ -45,6 +45,8 @@ 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

View file

@ -83,6 +83,7 @@ type NewTraining struct {
Description string `json:"description"` Description string `json:"description"`
Name string `json:"name"` Name string `json:"name"`
Pricing []TrainingPrice `json:"pricing"` Pricing []TrainingPrice `json:"pricing"`
Slug *string `json:"slug,omitempty"`
} }
// NewTrainingDate defines model for NewTrainingDate. // NewTrainingDate defines model for NewTrainingDate.
@ -138,6 +139,7 @@ type Training struct {
Id TrainingID `json:"id"` Id TrainingID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Pricing []TrainingPrice `json:"pricing"` Pricing []TrainingPrice `json:"pricing"`
Slug string `json:"slug"`
} }
// TrainingDate defines model for TrainingDate. // TrainingDate defines model for TrainingDate.
@ -2336,44 +2338,45 @@ func (sh *strictHandler) ListTrainingUpcomingDates(ctx *fiber.Ctx, trainingID Tr
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"H4sIAAAAAAAC/+xc3XLbuBV+FQzauzKi0t2d3dFV3Tjd9XQbu47di6a+gMkjEQkJMABoR+PRk/Sq79K+", "H4sIAAAAAAAC/+xczXLjuBF+FRSSW2hRk92t3dIpjj3ZdWUzdjx2Dpn4AJMtETMkwAFAe1QuPUlOeZfk",
"VwcASZEUKJL6sWSvrqJI4MF3/g8ODv2EA56knAFTEk+ecEoESUCBMP+7EYQyymbnRMGZUsBCgItz/UsI", "vVIASIqkQJH6s2StTqORwMbX/41G0y844EnKGTAl8egFp0SQBBQI8787QSijbHJJFJwrBSwEuLrUv4Qg",
"MhA0VZQzPCnXIb0QFSvRxTn2MNULUqIi7GFGEsATrNxkPSzga0YFhHiiRAYelkEECdH7/V7AFE/w7/wl", "A0FTRTnDo3Id0gtRsRJdXWIPU70gJSrCHmYkATzCyk3WwwK+ZlRAiEdKZOBhGUSQEL3f7wWM8Qj/zp/D",
"XN/+Kv0WlIuFV2OgG3gfvDvC2cC3FlsXrB1AMnAWmopMOZNgtH/BFAhG4vdCcKG/CDhTwJT+SNI0pgHR", "9e2v0m9BOZt5NQa6gffBuyWcDXxLsXXB2gIkA2emqciUMwlG+1dMgWAkfi8EF/qLgDMFTOmPJE1jGhAN",
"UP1U8PsYkj98lhr3U8+tr+xT56AIjaXdvi6AYn8EBsDCwxfsgcQ0vGBppg6HymBAVINYQvvA1V94xsJD", "1U8Ff4wh+cNnqXG/9Nz6xj51CYrQWNrt6wIo9kdgAMw8fMWeSEzDK5Zman+oDAZENYg5tA9c/YVnLNwX",
"wfrAFZpqAAWkRWEDRpfvBBAFLk+5hq8ZSIMzFTwFoahVv4ZB2Fx/VPNUG5xUgrKZ5hYSQmP9y5SLhCg8", "rA9cobEGUECaFTZgdHkhgChwecotfM1AGpyp4CkIRa36NQzCpvqjmqba4KQSlE00t5AQGutfxlwkROFR",
"yb/xVpdS+VFlYS6JEKYkixWeTEksoVx9z3kMhOnl1rYdW6YRZy2/cEmtFFZ+XFQd45Ml7pVYLUmv5LRC", "/o23uJTKjyoLc0mEMCZZrPBoTGIJ5epHzmMgTC+3tu3YMo04a/mFS2qlsPDjrOoYnyxxr8RqSXolpxVS",
"6q7Exu8/Q6D0PutkaN3GKDuOL6d48mm9Qj/Ao4sQXnhNNdzTOO42DxroZ3FEZE4q7CfuiMgrQnsutus2", "DyU2/vgZAqX3WSZD6zZG2XF8PcajT8sV+gGeXYTwzGuq4ZHGcbd50EA/iyMic1JhP3FHRN4Q2nOxXbdm",
"DMVVPdAQe5axJYI6+FX53zk10Gq9JAwFSOk0mICkJKBqXjNgytRP2MMJZTTJEjx5WyKgTMEMjJeHREHt", "KK7qgYbYs4zNEdTBL8r/wamBVuslYShASqfBBCQlAVXTmgFTpn7CHk4oo0mW4NG7EgFlCiZgvDwkCmpP",
"KfOFw+pDMpdO8uSbJf9D11ZUXrKY1ky+ooqY23jidghjDn1tRioi1A1N6owpmjgYa+gx535JIWe8gr4C", "mS8cVh+SqXSSJ98s+R+6tqLymsW0ZvIVVcTcxhO3Qxhz6GszUhGh7mhSZ0zRxMFYQ48593MKOeMV9BWo",
"1SuVUlFBgbafw23taA4HG2bVbmvuY66tprq9rdRSgcMg2oOqoIH+qMWgIJF9JVEaT06RCEHmbeE2N4kq", "XqmUigoKtP0cbmNHczjYalbttuY+5tpqqpvbSi0VOAyiPagKGuiPWgwKEtlXEqXx5BSJEGRqjCnOJr3j",
"yOXO3XrfSudb6XuIrn8G5Qp+73jGVJWDZo7NbGJsDUPjVX03IFkaLjm2YDplq+fJVg3xv6zYVQH/Mhzw", "cG4rVfRzSN0GsZExbGQIbSHNcO+2hZ9BuYLjBc+YqjLSzMGZTZytYWq4aA8NZJaGS5wtmE7Z7HWyWUP8",
"VyrVWRzfpgFPKJtV+ZZVDso41wzCKkc3JA7mammEQa96NNuc1dr5zuJzeXlzby0Il2W7pTDUP/rsuN1O", "byu2VcC/KT/8lUp1Hsf3acATyiZV9mWVkTIcNmO1ykGuEi5z7SxES1U7/q3Jce0YaPG5nL25txaEy8Dd",
"XTsUGt7/TptTd1GuOsWpBuiqAZqx71Tgv8ICv62uOPUiunoRV4UFNJwiKQrKVZ/IhAAWGBkC01b5Cb/7", "UljVTfrsuNlOXTsUGt79TutTd1Gu+sapVFi7VGjGxtMB4QgPCG11x6mX0dXLuCksoOEUSVFwLvpEJgSw",
"51+xh9/fXmMP3348r+yUP+fhb29m/E3+ZcIZzEfvCkKVH9+kJPhCZhYRURGe4BlVMbkfJUSAjL6OAp74", "wMgQmLbKT/jin3/FHn5/f4s9fP/xsrJT/pyHv51N+Fn+ZcIZTAcXBaHKj2cpCb6QiUVEVIRHeEJVTB4H",
"0Vd/PpuFgkga+zTv7vmGatF+rJS1y21yrtxyqDXGVpqoH40zIBURhQIiBAWJVAQotA8gPkWE2ZYZokx/", "CREgo6+DgCd+9NWfTiahIJLGPs27g76hWrQvK2XvfJucK7ccao21hSbsR+MMSEVEoYAIQUEiFQEK7QOI",
"/uXm5goVfdAR+giAIqVSOfH9kCiiBAm+gBhRUNMRFzM/5IEfqST2xTT48afxj2jKBUq4AESZtUjK2ehf", "jxFhtuWGKNOff7m7u0FFH3WAPgKgSKlUjnw/JIooQYIvIAYU1HjAxcQPeeBHKol9MQ5+/Gn4IxpzgRIu",
"Nt7VQr6BsAr5DEVZQtgbASQk9zEg+JbGhBk6SKYQ0CkNkOJIRVQiHuSCAs2LZi3vMI6cDsCkIsxaTnPX", "AFFmLZJyNviXjXe1XGAgLEI+R1GWEHYmgITkMQYE39KYMEMHyRQCOqYBUhypiErEg1xQoHnRrOUdyoHT",
"2+sLJGAKlpiRGNW+QqeF0MrNh20qFVGZQzc3EVhp2wUo4CGgGTAQREGI7ueGMhd0RhmSIB5AGOH25rsS", "AZhUhFnLae56f3uFBIzBEjMSo9pX6LgQWrn5aptKRVTm0M1dBFbadgEKeAhoAgwEURCix6mhzAWdUIYk",
"bBVVMfSQtcyShIh5gybSBJ282S82EWYH6WYtqH8t2ChF6hVGVNGsy0uqdcfxVvDNfH/8h6S25HU6Ve/v", "iCcQRri9+a4EW0VVDD1kLbMkIWLaoIk0QSdv9ot1hNlBulkk6l8LNkqReoURVTTr8pJqQXLwFX4z7R/+",
"VO0ib//V5n+bhmu7whXBNHlr4CPtMLw1F6Nrar56Mi1Od6PaDegyo9Ik5UINSKjpl5mvSp9tAXlF5gkw", "Waoth50O37s7fLvI23+1F9yn4dLmckUwTd4a+Eg7DG/J/eqS0q+eU4vT36B2kTpPrDRJuVAr5NX0y8RX",
"1SGmNDeHDhGZZV3i2VQs5WXrPkSyKai9ASrruX6ho3D2puKKdFQUeJdX7z9gD7+7vL66vD67eb9a4TkT", "peu2gLwh0wSY6hBTmptDh4jMsi7xrCuW8s52FyJZF9TOAJVlXb/QUTh7U3FFVirqvOub9x+why+ub2+u",
"jdvzrNGc7hC3qdvXyfDUlX2O/LGqgdMd4ituMbjU/ZL68HX8pzvEvv3DptyO/wpjYc7nU14MD5HA6DlP", "b8/v3i8Wes584/Y8azSnq8hNyvdlMjw1b18jfyxq4HQVecSdBpe631K7vo7/dBW5cX+xKdA3cwUyM8f4",
"k5iS5E8P/LOCIDLlhS4ulrNg/+Cf1X//HUTob0TA//6DPZwJ/VTRunh8fBytPK1jAA0gl0tO6UrwVFBQ", "MS9mlEhg7CBPo5iS5E9P/LOCIDLlhy4+5iNn/+Cf1X//HUTob0TA//6DPZwJ/VTR4Xh+fh4sPK1jBA0g",
"RMwrB2es6cpf/o7Ori6whx9ASHvUHY/ejsZ6IU+BkZTiCf5uNB6NtXqIioy0/Ie3ZdVjvpiBYU1L1Lj1", "F09O6UbwVFBQREwr52us6cpf/o7Ob66wh59ASHsiHg7eDYZ6IU+BkZTiEf5uMBwMtZaIiozQ/Kd3ZVVk",
"RYgn9c47bsyi/XE8XjNUNWyYyt3id8xUnaGYSmV6ASWuhYd/sFhcW5SY/frwnJnGsq2FnFNE4rhC1sOK", "vpiAYU0L1rj9VYhH9c49boy8/XE4XDK7tdrMlvuKwDG6dY5iKpVpGZS4Zh7+wWJxbVFi9uszemboy3Yg",
"aNl8wksR3Nm6xCGq+gV5Pv0HUv2Zh/Odick9vdDo0ymRwWJFV2/3BqJdWeW4ZGAeCbWmvu+nqeZQ4S50", "ck4RieMKWQ8romXzCc9F8GDrFoeo6tft+ZAhSPVnHk63Jib3kESjnadEBrMFXb3bGYh2ZZVTmYF5JNSa",
"bIEjghg8lnpuUfPCq7uIn+VXXG/KW8lWj2m78ty3A3VetTpU9GvuTVXTRwWvyPJqtPZ9t+zr85Y79co6", "+r6fppqzi9vQsQWOCGLwXOq5Rc0zr+4ifpZfkZ2Vt5qtHtN2ZbprB+q8qnWo6Nfcm6qmjwpekeXVaO37",
"oiZg2VeJT8v724WtjWOwlV9di+fm+4ozV2ewW7LUckkt0dytKP37NXPFFlB4QJFb1hFZWsP93I47u6Oh", "btnXxzq36pV1RE3Asq8SX+b3vzNbO8dgK8O6Fi/N9xVnro56tySr+ZJavnlYUPr3S8aXLaBwjyK3rCMy",
"0wsqkwo7Ft7uPMY1TeFMOKpygXwgpfwMqr9G0syhkXrttb1Sdp/b3FV1r9w23huIHrktM49smdsOZFeW", "t4bHqZ2qdkdDpxdUBh62LLzteYxrKMOZcFTlAnpPSvkZVH+NpJlDI/USbHOlbD+3uavuXrltuDMQPXJb",
"3b6mtTac+t15sZkMjzMutE/OrEmfpfyOJmMuE2VXqdOvoj23PYGjCxvt8+cHLYtrXY114cM44BHXx6ho", "Zh7ZMLftya4su31Na2k49bvzYjMZHmZcaJ+8WZI+S/kdTMacJ8quUqdfRXtpewYHFzbax9z3WhbXuh7L",
"hW0WEJZf2S7LgLJra4vzeq8uOkADizUtrWOs2Ayw7cq2oxD+Xoq9Ls88upDeKP56KLdHBXgo/e67bhyc", "wodxwAOuj1HRKlsvIMy/sl2YFcqujS3O67266BCtWKxpaR1ixWaAbVa2HYTwd1LsdXnmwYX0RvHXQ7k9",
"AMZ7BdI3AbyuIrLTRDdJHD4phqZ7F5nlmPUrimTdo+R9YxpaCvQI6tUSTL1mXVd+9C9cy3vYVxHxul8c", "KsB96XfXdePKCWC4UyB9E8BxFZGdJrpO4vBJMXTdu8gsx7SPKJJ1j6L3jWloLtADqFdLMPWadVn50b9w",
"Pnjpu3KD3hkBC/3vphY+kDW3V9Alf3uJiH750lqfuq72ItzrLfLc7/s5DNEsOM7w6Kr9lp6S628P9vTk", "Le9pjyLidb+fvPfSd+GGvTMCFvrfTi28J2tur6BL/nYSEf3y3bc+dV3tfbrjLfLcrw06DNEsOMzw6Kr9",
"/nsgA09vBwq7w56ozrNscP4rtXG0B8ES4fYnwleg0L2Hm+Ep71jjy04OmS/TZJ7jmLpR0TZ+FkCDi7ZX", "5p6S628H9vTi/rMjK57e9hR2V3uiOu+yxvmv1MbBHgRLhJufCI9AoTsPN6unvEONL1s5ZL5Nk3mNY+pa",
"eH7t5QZ7SrN+dSh+oKMtB+9/6y435LWEl+x8pOSpeG3rFbhjzslqM8DF9zN7Z/EuxkDPzF/0OLllz3dg", "RdvwVQCtXLQd4fm1lxvsKM361aH5FR1tPpj/W3e5VV5beMvOR0qeire7jsAdc04WmwEuvl/ZO4t3NVb0",
"XrJPppah35ZDpqWBD/fGAfN1zr868SLmCdx/J+PFzBU4pvA65yg1FfOesFVKg00ekBiF8AAxT62/mLW1", "zPxFkJNb9nxH5i37ZGoZ+m05ZFoa+OreuMJ8nfOvVryJeQL339l4M3MFjim8zjlKTcW8TmyV0mCTByRG",
"WemJ78d6XcSlmnw3Ho+NvvLNmhQvC2uRiNzzTNVmBPNx6iW+xd3i/wEAAP//jH42mm5VAAA=", "ITxBzFPrL2ZtbVZ65PuxXhdxqUbfDYdDo698sybF68JaJCKPPFO1GcF8nHqOb/Yw+38AAAD//5Sh0RPV",
"VQAA",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View file

@ -79,8 +79,14 @@ func (h *APIHandlers) CreateTraining(ctx context.Context, req CreateTrainingRequ
} }
} }
slug := ""
if req.Body.Slug != nil {
slug = *req.Body.Slug
}
t := training.Training{ t := training.Training{
Name: req.Body.Name, Name: req.Body.Name,
Slug: slug,
Days: req.Body.Days, Days: req.Body.Days,
Description: req.Body.Description, Description: req.Body.Description,
Pricing: pricing, Pricing: pricing,
@ -108,6 +114,7 @@ func (h *APIHandlers) CreateTraining(ctx context.Context, req CreateTrainingRequ
return CreateTraining201JSONResponse{ return CreateTraining201JSONResponse{
Id: t.ID, Id: t.ID,
Name: t.Name, Name: t.Name,
Slug: t.Slug,
Days: t.Days, Days: t.Days,
Description: t.Description, Description: t.Description,
Pricing: responsePricing, Pricing: responsePricing,
@ -162,6 +169,7 @@ func (h *APIHandlers) GetTraining(ctx context.Context, req GetTrainingRequestObj
return GetTraining200JSONResponse{ return GetTraining200JSONResponse{
Id: t.ID, Id: t.ID,
Name: t.Name, Name: t.Name,
Slug: t.Slug,
Days: t.Days, Days: t.Days,
Description: t.Description, Description: t.Description,
Pricing: pricing, Pricing: pricing,
@ -187,9 +195,16 @@ func (h *APIHandlers) UpdateTraining(ctx context.Context, req UpdateTrainingRequ
Type: training.PriceType(p.Type), Type: training.PriceType(p.Type),
} }
} }
slug := ""
if req.Body.Slug != nil {
slug = *req.Body.Slug
}
t := training.Training{ t := training.Training{
ID: req.TrainingID, ID: req.TrainingID,
Name: req.Body.Name, Name: req.Body.Name,
Slug: slug,
Days: req.Body.Days, Days: req.Body.Days,
Description: req.Body.Description, Description: req.Body.Description,
Pricing: pricing, Pricing: pricing,
@ -208,6 +223,7 @@ func (h *APIHandlers) UpdateTraining(ctx context.Context, req UpdateTrainingRequ
return UpdateTraining200JSONResponse{ return UpdateTraining200JSONResponse{
Id: t.ID, Id: t.ID,
Name: t.Name, Name: t.Name,
Slug: t.Slug,
Days: t.Days, Days: t.Days,
Description: t.Description, Description: t.Description,
}, nil }, nil

View file

@ -9,6 +9,7 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gitlab.mareshq.com/hq/yggdrasil/internal/money" "gitlab.mareshq.com/hq/yggdrasil/internal/money"
"gitlab.mareshq.com/hq/yggdrasil/pkg/slug"
"gitlab.mareshq.com/hq/yggdrasil/pkg/training" "gitlab.mareshq.com/hq/yggdrasil/pkg/training"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -115,6 +116,8 @@ func TestServer(t *testing.T) {
}, },
} }
slugString := slug.NewString(newTraining.Name)
rr, _ := doPost(t, app, "/v1/trainings", newTraining) rr, _ := doPost(t, app, "/v1/trainings", newTraining)
assert.Equal(t, http.StatusCreated, rr.StatusCode) assert.Equal(t, http.StatusCreated, rr.StatusCode)
@ -122,6 +125,7 @@ func TestServer(t *testing.T) {
err := json.NewDecoder(rr.Body).Decode(&resultTraining) err := json.NewDecoder(rr.Body).Decode(&resultTraining)
assert.NoError(t, err, "error unmarshalling response") assert.NoError(t, err, "error unmarshalling response")
assert.Equal(t, newTraining.Name, resultTraining.Name) assert.Equal(t, newTraining.Name, resultTraining.Name)
assert.Equal(t, slugString, resultTraining.Slug)
assert.Equal(t, newTraining.Description, resultTraining.Description) assert.Equal(t, newTraining.Description, resultTraining.Description)
assert.Equal(t, newTraining.Days, resultTraining.Days) assert.Equal(t, newTraining.Days, resultTraining.Days)
assert.Equal(t, newTraining.Pricing, resultTraining.Pricing) assert.Equal(t, newTraining.Pricing, resultTraining.Pricing)
@ -177,8 +181,10 @@ func TestServer(t *testing.T) {
_ = handlers.trainingRepository.Create(tr) _ = handlers.trainingRepository.Create(tr)
updatedSlug := "updated-training"
updTr := NewTraining{ updTr := NewTraining{
Name: "Updated Training", Name: "Updated Training",
Slug: &updatedSlug,
Description: tr.Description, Description: tr.Description,
Days: tr.Days, Days: tr.Days,
Pricing: []TrainingPrice{ Pricing: []TrainingPrice{
@ -197,6 +203,7 @@ func TestServer(t *testing.T) {
err := json.NewDecoder(rr.Body).Decode(&trr) err := json.NewDecoder(rr.Body).Decode(&trr)
assert.NoError(t, err, "error unmarshalling response") assert.NoError(t, err, "error unmarshalling response")
assert.Equal(t, updTr.Name, trr.Name) assert.Equal(t, updTr.Name, trr.Name)
assert.Equal(t, updatedSlug, trr.Slug)
}) })
t.Run("Delete training", func(t *testing.T) { t.Run("Delete training", func(t *testing.T) {

View file

@ -5,6 +5,7 @@ CREATE SCHEMA IF NOT EXISTS training;
CREATE TABLE IF NOT EXISTS training.trainings ( CREATE TABLE IF NOT EXISTS training.trainings (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name varchar(255) NOT NULL, name varchar(255) NOT NULL,
slug varchar(255) NOT NULL,
description text NOT NULL, description text NOT NULL,
days smallint NOT NULL days smallint NOT NULL
); );

47
pkg/slug/slug.go Normal file
View file

@ -0,0 +1,47 @@
package slug
import (
"errors"
"github.com/gosimple/unidecode"
"regexp"
"strings"
)
type Slug string
var (
ErrInvalidSlug = errors.New("invalid slug")
regexpNonAuthorizedChars = regexp.MustCompile("[^a-zA-Z0-9-_]")
regexpMultipleDashes = regexp.MustCompile("-+")
)
func New(s string) Slug {
s = unidecode.Unidecode(s)
s = strings.ToLower(s)
s = regexpNonAuthorizedChars.ReplaceAllString(s, "-")
s = regexpMultipleDashes.ReplaceAllString(s, "-")
s = strings.Trim(s, "-_")
return Slug(s)
}
func NewString(s string) string {
return New(s).String()
}
func Validate(s string) error {
if s == "" {
return ErrInvalidSlug
}
if s == NewString(s) {
return nil
}
return ErrInvalidSlug
}
func (s Slug) String() string {
return string(s)
}

59
pkg/slug/slug_test.go Normal file
View file

@ -0,0 +1,59 @@
package slug
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNew(t *testing.T) {
t.Run("Test New", func(t *testing.T) {
s := New("Hello World")
assert.Equal(t, "hello-world", s.String())
})
t.Run("Test New with !", func(t *testing.T) {
s := New("Hello World!")
assert.Equal(t, "hello-world", s.String())
})
t.Run("Test New with unicode", func(t *testing.T) {
s := New("Ahoj Světe!") // hello world in Czech
assert.Equal(t, "ahoj-svete", s.String())
})
}
func TestValidate(t *testing.T) {
t.Run("Test Validate", func(t *testing.T) {
assert.NoError(t, Validate("hello-world"))
})
t.Run("Test Validate with !", func(t *testing.T) {
assert.ErrorIs(t, Validate("hello-world!"), ErrInvalidSlug)
})
t.Run("Test Validate with unicode", func(t *testing.T) {
assert.ErrorIs(t, Validate("ahoj-světe"), ErrInvalidSlug)
})
t.Run("Test Validate with empty string", func(t *testing.T) {
assert.ErrorIs(t, Validate(""), ErrInvalidSlug)
})
}
func TestSlug_String(t *testing.T) {
t.Run("Test String", func(t *testing.T) {
s := Slug("hello-world")
assert.Equal(t, "hello-world", s.String())
})
}
func TestNewString(t *testing.T) {
t.Run("Test NewString", func(t *testing.T) {
input := "Hello World"
expected := "hello-world"
stringSlug := NewString(input)
assert.Equal(t, expected, stringSlug)
assert.IsType(t, string(""), stringSlug)
})
}

View file

@ -1,6 +1,7 @@
package training package training
import ( import (
"gitlab.mareshq.com/hq/yggdrasil/pkg/slug"
"sync" "sync"
"time" "time"
) )
@ -24,6 +25,16 @@ func (r *InMemoryTrainingRepository) Create(training *Training) error {
training.ID = r.ai training.ID = r.ai
r.ai++ r.ai++
if training.Slug == "" {
training.Slug = slug.NewString(training.Name)
} else {
slugValidateErr := slug.Validate(training.Slug)
if slugValidateErr != nil {
return slugValidateErr
}
}
r.trainings[training.ID] = *training r.trainings[training.ID] = *training
return nil return nil
} }
@ -54,6 +65,15 @@ func (r *InMemoryTrainingRepository) Update(training *Training) error {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
if training.Slug == "" {
training.Slug = slug.NewString(training.Name)
} else {
slugValidateErr := slug.Validate(training.Slug)
if slugValidateErr != nil {
return slugValidateErr
}
}
r.trainings[training.ID] = *training r.trainings[training.ID] = *training
return nil return nil
} }

View file

@ -11,6 +11,7 @@ type ID = int
type Training struct { type Training struct {
ID ID ID ID
Name string Name string
Slug string
Days int8 Days int8
Description string Description string
Pricing []Price `db:"-"` Pricing []Price `db:"-"`

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"gitlab.mareshq.com/hq/yggdrasil/pkg/slug"
"time" "time"
) )
@ -19,16 +20,20 @@ func (r *PostgresTrainingRepository) Create(training *Training) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if training.Slug == "" {
training.Slug = slug.NewString(training.Name)
}
tx, txErr := r.pg.Begin(ctx) tx, txErr := r.pg.Begin(ctx)
if txErr != nil { if txErr != nil {
return txErr return txErr
} }
queryErr := tx.QueryRow(ctx, ` queryErr := tx.QueryRow(ctx, `
INSERT INTO training.trainings (name, description, days) INSERT INTO training.trainings (name, slug, description, days)
VALUES ($1, $2, $3) VALUES ($1, $2, $3, $4)
RETURNING id RETURNING id
`, training.Name, training.Description, training.Days).Scan(&training.ID) `, training.Name, training.Slug, training.Description, training.Days).Scan(&training.ID)
if queryErr != nil { if queryErr != nil {
return queryErr return queryErr
} }
@ -75,7 +80,7 @@ func (r *PostgresTrainingRepository) FindByID(id ID) (*Training, error) {
SELECT id, name, description, days SELECT id, name, description, days
FROM training.trainings FROM training.trainings
WHERE id = $1 WHERE id = $1
`, id).Scan(&training.ID, &training.Name, &training.Description, &training.Days) `, id).Scan(&training.ID, &training.Name, &training.Slug, &training.Description, &training.Days)
if err != nil { if err != nil {
return nil, err return nil, err
@ -122,7 +127,7 @@ func (r *PostgresTrainingRepository) FindAll() ([]Training, error) {
trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) { trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) {
var training Training var training Training
scanErr := row.Scan(&training.ID, &training.Name, &training.Description, &training.Days) scanErr := row.Scan(&training.ID, &training.Name, &training.Slug, &training.Description, &training.Days)
if scanErr != nil { if scanErr != nil {
return Training{}, scanErr return Training{}, scanErr
} }
@ -151,6 +156,15 @@ func (r *PostgresTrainingRepository) Update(training *Training) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if training.Slug == "" {
training.Slug = slug.NewString(training.Name)
} else {
slugValidateErr := slug.Validate(training.Slug)
if slugValidateErr != nil {
return slugValidateErr
}
}
tx, err := r.pg.Begin(ctx) tx, err := r.pg.Begin(ctx)
if err != nil { if err != nil {
return err return err
@ -158,9 +172,9 @@ func (r *PostgresTrainingRepository) Update(training *Training) error {
_, err = tx.Exec(ctx, ` _, err = tx.Exec(ctx, `
UPDATE training.trainings UPDATE training.trainings
SET name = $1, description = $2, days = $3 SET name = $1, slug = $2, description = $3, days = $4
WHERE id = $4 WHERE id = $4
`, training.Name, training.Description, training.Days, training.ID) `, training.Name, training.Slug, training.Description, training.Days, training.ID)
if err != nil { if err != nil {
return err return err