From b0ea7d57b6006f7081ae61d38bfc3ce795a40552 Mon Sep 17 00:00:00 2001
From: froge <froge@git.repo.cafe>
Date: Wed, 12 Feb 2025 03:25:08 +1000
Subject: [PATCH] Add spotify auth handling and route testing, begin DB work

---
 .gitignore     |  1 +
 databse.go     | 39 +++++++++++++++++++++++++++++++++++++++
 go.mod         | 14 ++++++++++++--
 go.sum         | 22 ++++++++++++++++++++--
 main.go        | 28 ++++++++++++++++------------
 routes.go      | 34 +++++++++++++++++-----------------
 routes_test.go | 30 ++++++++++++++++++++++++++++++
 7 files changed, 135 insertions(+), 33 deletions(-)
 create mode 100644 databse.go
 create mode 100644 routes_test.go

diff --git a/.gitignore b/.gitignore
index 4c49bd7..92819e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 .env
+gomusic.db
diff --git a/databse.go b/databse.go
new file mode 100644
index 0000000..22ebb8a
--- /dev/null
+++ b/databse.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+    "gorm.io/gorm"
+    "gorm.io/driver/sqlite"
+) 
+
+type ArtistInfo struct {
+    gorm.Model
+    Name string
+    Popularity string
+    SpotifyID string
+    ArtistGenre ArtistGenre
+}
+
+// Stores an association between several Genres and an individual artist
+type ArtistGenre struct {
+    gorm.Model
+    Genres []Genre
+    ArtistInfoID uint
+}
+
+type Genre struct {
+    gorm.Model
+    Genre string
+    ArtistGenreID uint
+}
+
+func setupDatabase() *gorm.DB  {
+    db, err := gorm.Open(sqlite.Open("gomusic.db"), &gorm.Config{})
+    if err != nil {
+    	panic("Failed to open database")
+    }
+
+    // Run model migrations here to keep DB consistent
+    db.AutoMigrate(&ArtistInfo{}, &ArtistGenre{}, &Genre{})
+
+    return db
+}
diff --git a/go.mod b/go.mod
index eb043e6..25de27d 100644
--- a/go.mod
+++ b/go.mod
@@ -2,25 +2,33 @@ module git.repo.cafe/froge/gomusic
 
 go 1.23.5
 
+require (
+	github.com/gin-gonic/gin v1.10.0
+	github.com/stretchr/testify v1.10.0
+)
+
 require (
 	github.com/bytedance/sonic v1.12.8 // indirect
 	github.com/bytedance/sonic/loader v0.2.3 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
-	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/gin-contrib/sse v1.0.0 // indirect
-	github.com/gin-gonic/gin v1.10.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.24.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.24 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	golang.org/x/arch v0.14.0 // indirect
@@ -30,4 +38,6 @@ require (
 	golang.org/x/text v0.22.0 // indirect
 	google.golang.org/protobuf v1.36.5 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	gorm.io/driver/sqlite v1.5.7 // indirect
+	gorm.io/gorm v1.25.12 // indirect
 )
diff --git a/go.sum b/go.sum
index d60e044..e79e143 100644
--- a/go.sum
+++ b/go.sum
@@ -5,9 +5,9 @@ github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wio
 github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
 github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
-github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
 github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
@@ -15,6 +15,8 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
 github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -23,7 +25,13 @@ github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE
 github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -34,6 +42,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -41,6 +51,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -52,6 +63,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
@@ -68,11 +80,17 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
 nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/main.go b/main.go
index 9cabfdb..e4735d9 100644
--- a/main.go
+++ b/main.go
@@ -10,8 +10,19 @@ import (
 var spotifyClientID = os.Getenv("SPOTIFY_ID")
 var spotifyClientSecret = os.Getenv("SPOTIFY_SECRET")
 
-// DB related setup
-var db = make(map[string]string)
+// Make sure the DB is in a good state to launch
+var db = setupDatabase()
+
+func setupRouter() *gin.Engine {
+    var r *gin.Engine = gin.Default()
+    
+    // Add middleware to handle spotify auth for us
+    r.Use(spotifyAuth(spotifyClientID, spotifyClientSecret))
+    
+    r.GET("/ping", ping)
+    r.GET("/artists/:artistID", getArtistByID)
+    return r
+}
 
 func main() {
     // If the auth/ID variables are empty something is probably misconfigured
@@ -21,15 +32,8 @@ func main() {
     if spotifyClientSecret == "" {
         slog.Warn("[GOMUSIC] No Spotify secret configured in 'SPOTIFY_SECRET' environment variable")
     }
-    
-    // API server setup
-    var r *gin.Engine = gin.Default()
-    
-    // Add middleware to handle spotify auth for us
-    r.Use(spotifyAuth(spotifyClientID, spotifyClientSecret))
-    
-    r.GET("/ping", ping)
-    r.GET("/artists/:artistID", getArtistByID)
-    
+
+    // Router and server setup
+    r := setupRouter()
     r.Run(":8080")
 }
diff --git a/routes.go b/routes.go
index c0744dc..8747169 100644
--- a/routes.go
+++ b/routes.go
@@ -9,14 +9,14 @@ import (
     "io"
 )
 
-type ArtistInfo struct {
-	ID string
-	Name string
-	Popularity int
-	Genres []string
+type SpotifyResponse struct {
+       ID string
+       Name string
+       Popularity int
+       Genres []string
 }
 
-// Define our route functions here
+
 func ping(c *gin.Context) {
     c.String(http.StatusOK, "pong")
 }
@@ -26,7 +26,7 @@ func getArtistByID(c *gin.Context) {
     spotifyAuthToken := c.GetString("spotifyAuthToken")
 
     if artistID == "" || spotifyAuthToken == "Bearer " {
-    	c.JSON(http.StatusBadRequest, gin.H{"Error": "Could not find required parameters and/or required authentication tokens"})
+		c.JSON(http.StatusBadRequest, gin.H{"Error": "Could not find required parameters and/or required authentication tokens"})
 	return
     }
 
@@ -34,27 +34,27 @@ func getArtistByID(c *gin.Context) {
     artistEndpoint := fmt.Sprintf("https://api.spotify.com/v1/artists/%s", artistID)
     req, err := http.NewRequest("GET", artistEndpoint, nil)
     if err != nil {
-    	slog.Error("[GOMUSIC] Failed to build HTTP request", "Error", err)
+		slog.Error("[GOMUSIC] Failed to build HTTP request", "Error", err)
 	c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to request latest spotify data"})
 	return
     }
     req.Header.Add("Authorization", spotifyAuthToken)
 
-    // Send request and save the response into DB
+    // Send off request
     resp, err := http.DefaultClient.Do(req)
     if err != nil {
-    	slog.Error("[GOMUSIC] Failed to get artist data from spotify API", "Error", err)
+		slog.Error("[GOMUSIC] Failed to get artist data from spotify API", "Error", err)
 	c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to request latest spotify data"})
 	return
     } else if resp.StatusCode != 200 {
-    	slog.Error("[GOMUSIC] Failed to get artist data from spotify API", "Error", resp.Status)
+		slog.Error("[GOMUSIC] Failed to get artist data from spotify API", "Error", resp.Status)
 	c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to request latest spotify data"})
 	return
 
     }
     respData, err := io.ReadAll(resp.Body)
     if err != nil {
-    	slog.Error("[GOMUSIC] Failed to read response data from spotify API", "Error", resp.Status)
+		slog.Error("[GOMUSIC] Failed to read response data from spotify API", "Error", resp.Status)
 	c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to request latest spotify data"})
 	return
 
@@ -63,17 +63,14 @@ func getArtistByID(c *gin.Context) {
     // Close this immediately since it's unused now
     resp.Body.Close()
 
-    var artistInfo ArtistInfo
-    err = json.Unmarshal(respData, &artistInfo)
+    var spotifyResponse SpotifyResponse
+    err = json.Unmarshal(respData, &spotifyResponse)
     if err != nil {
         slog.Error("[GOMUSIC] Failed to read response body data from spotify", "Error", err)
 	c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to request latest spotify data"})
 	return
     }
     
-    // Send back our response
-    c.JSON(http.StatusOK, artistInfo)
-
     // Update DB here
     //value, ok := db[artistID]
     //if ok {
@@ -81,4 +78,7 @@ func getArtistByID(c *gin.Context) {
     //} else {
     //    c.JSON(http.StatusOK, gin.H{"artistID": artistID, "status": "no value"})
     //}
+
+    // Send back our response data
+    c.JSON(http.StatusOK, spotifyResponse)
 }
diff --git a/routes_test.go b/routes_test.go
new file mode 100644
index 0000000..ee4de8f
--- /dev/null
+++ b/routes_test.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestPingRoute(t *testing.T) {
+	router := setupRouter()
+
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("GET", "/ping", nil)
+	router.ServeHTTP(w, req)
+
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, "pong", w.Body.String())
+}
+
+func TestGetArtistByIDRoute(t *testing.T) {
+	router := setupRouter()
+
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("GET", "/artists/0TnOYISbd1XYRBk9myaseg", nil)
+	router.ServeHTTP(w, req)
+
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, `{"ID":"0TnOYISbd1XYRBk9myaseg","Name":"Pitbull","Popularity":83,"Genres":[]}`, w.Body.String())
+}