added validation when adding a book

This commit is contained in:
2025-09-24 17:06:39 +02:00
parent 8432902df1
commit 2f0a9b5127
7 changed files with 105 additions and 13 deletions

View File

@@ -63,6 +63,26 @@ func TestPostBookHandler_WrongRating(t *testing.T) {
testPostBookHandler(t, bookJson, 400)
}
func TestPostBookHandler_TitleTooLong(t *testing.T) {
bookJson :=
`{
"title": "Noisy outlaws, unfriendly blobs, and some other things that aren't as scary, maybe, depending on how you feel about lost lands, stray cellphones, creatures from the sky, parents who disappear in Peru, a man named Lars Farf, and one other story we couldn't quite finish, so maybe you could help us out.",
"author": "Eli Horowitz",
"rating": 2
}`
testPostBookHandler(t, bookJson, 400)
}
func TestPostBookHandler_AuthorTooLong(t *testing.T) {
bookJson :=
`{
"title": "something",
"author": "Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflegeundsorgfaltigkeitbeschutzenvonangreifendurchihrraubgierigfeindewelchevoralternzwolftausendjahresvorandieerscheinenvanderersteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenachdiesternwelchegehabtbewohnbarplanetenkreisedrehensichundwohinderneurassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemitnichteinfurchtvorangreifenvonandererintelligentgeschopfsvonhinzwischensternartigraum",
"rating": 2
}`
testPostBookHandler(t, bookJson, 400)
}
func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) {
router := testSetup()
w := httptest.NewRecorder()

View File

@@ -25,3 +25,4 @@ INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Sa majesté
INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Le Pavillon d''or','Yukio Mishima', 8);
INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Le meurtre d''O-tsuya','Junichiro Tanizaki', 7);
INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Dojoji et autres nouvelles','Yukio Mishima', 8);
INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Noisy outlaws, unfriendly blobs, and some other things that aren''t as scary, maybe, depending on how you feel about lost lands, stray cellphones, creatures from the sky, parents who disappear in Peru, a man named Lars Farf, and one other story we couldn''t quite finish, so maybe you could help us out','Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepf', 2);

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue'
import { ref, reactive, computed } from 'vue'
import { postBook } from './api.js'
import { useRouter, useRoute } from 'vue-router'
@@ -9,10 +9,36 @@
title: "",
author: ""
});
const errors = ref(null)
const titleError = computed(() => {
return extractErrorFromField("Title");
})
const authorError = computed(() => {
return extractErrorFromField("Author");
})
function extractErrorFromField(fieldName) {
if (errors.value === null) {
return "";
}
const titleErr = errors.value.find((e) => e["field"] === fieldName);
if (typeof titleErr !== 'undefined') {
return titleErr.error
} else {
return "";
}
}
function onSubmit(e) {
postBook(book);
router.push('/');
postBook(book)
.then((res) => {
if (res.ok) {
router.push('/');
return;
} else {
res.json().then((json) => (errors.value = json));
}
})
}
</script>
@@ -21,14 +47,17 @@
<div class="field">
<label class="label">Title</label>
<div class="control">
<input class="input" type="text" v-model="book.title" placeholder="Title">
<input :class="'input ' + (titleError ? 'is-danger' : '')" type="text" maxlength="300"
required v-model="book.title" placeholder="Title">
</div>
<p v-if="titleError" class="help is-danger">{{titleError}}</p>
</div>
<div class="field">
<label class="label">Author</label>
<div class="control">
<input class="input" type="text" v-model="book.author" placeholder="Author">
<input :class="'input ' + (authorError ? 'is-danger' : '')" type="text" maxlength="100" v-model="book.author" placeholder="Author">
</div>
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
</div>
<div class="field">
<div class="control">

View File

@@ -3,13 +3,13 @@ import { ref } from 'vue'
const baseUrl = "http://localhost:8080"
function useFetch(url) {
const data = ref(null)
const error = ref(null)
const data = ref(null);
const error = ref(null);
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
.catch((err) => (error.value = err));
return { data, error }
}
@@ -19,11 +19,11 @@ export function getBooks() {
}
export function postBook(book) {
fetch(baseUrl + '/book', {
return fetch(baseUrl + '/book', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(book.value)
});
})
}

View File

@@ -1,7 +1,7 @@
package api
type bookPostCreate struct {
Title string `json:"title" binding:"required"`
Author string `json:"author"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Rating int `json:"rating" binding:"min=0,max=10"`
}

View File

@@ -1,10 +1,12 @@
package api
import (
"errors"
"net/http"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
)
@@ -18,7 +20,12 @@ func PostBookHandler(c *gin.Context, db *gorm.DB) {
var book bookPostCreate
err := c.ShouldBindJSON(&book)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
var ve validator.ValidationErrors
if errors.As(err, &ve) {
c.JSON(http.StatusBadRequest, getValidationErrors(&ve))
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return
}
bookDb := book.toBook()

35
internal/api/validator.go Normal file
View File

@@ -0,0 +1,35 @@
package api
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type apiValidationError struct {
Field string `json:"field"`
Err string `json:"error"`
}
func getValidationErrors(ve *validator.ValidationErrors) []apiValidationError {
errors := make([]apiValidationError, len(*ve))
for i, fe := range *ve {
errors[i] = apiValidationError{
Field: fe.Field(),
Err: computeValidationMessage(&fe),
}
}
return errors
}
func computeValidationMessage(fe *validator.FieldError) string {
tag := (*fe).Tag()
switch tag {
case "required":
return fmt.Sprintf("%s is required.", (*fe).Field())
case "max":
return fmt.Sprintf("%s is too long. It should be under %s characters.", (*fe).Field(), (*fe).Param())
default:
return fmt.Sprintf("Validation failed for '%s' property.", tag)
}
}