From f0ba49024c5e443c3c977cb51603b3332659b6c5 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Wed, 20 Dec 2023 16:28:48 +0530 Subject: [PATCH] feat(api): add endpoints to modify obligation maps Signed-off-by: Gaurav Mishra --- cmd/laas/docs/docs.go | 161 ++++++++++++++++++++ cmd/laas/docs/swagger.json | 163 +++++++++++++++++++- cmd/laas/docs/swagger.yaml | 107 +++++++++++++ go.mod | 5 +- go.sum | 15 +- pkg/api/api.go | 5 + pkg/api/obligationmap.go | 299 ++++++++++++++++++++++++++++++++++++- pkg/models/types.go | 16 ++ 8 files changed, 758 insertions(+), 13 deletions(-) diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index ecd3512..84b6d85 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -543,6 +543,128 @@ const docTemplate = `{ } } }, + "/obligation_maps/topic/{topic}/license": { + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Replaces the license list of an obligation topic with the given list in the obligation map.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Change license list", + "operationId": "UpdateLicenseInObligationMap", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation", + "name": "topic", + "in": "path", + "required": true + }, + { + "description": "Shortnames of the licenses to be in map", + "name": "shortnames", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseShortnamesInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationMapResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No license or obligation found.", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Add or remove licenses from obligation map for a given obligation topic", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Add or remove licenses from obligation map", + "operationId": "PatchObligationMap", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation", + "name": "topic", + "in": "path", + "required": true + }, + { + "description": "Shortnames of the licenses with action", + "name": "shortname", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseMapShortnamesInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationMapResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No license or obligation found.", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Failure to insert new maps", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, "/obligations": { "get": { "description": "Get all active obligations from the service", @@ -1229,6 +1351,30 @@ const docTemplate = `{ } } }, + "models.LicenseMapShortnamesElement": { + "type": "object", + "properties": { + "add": { + "type": "boolean", + "example": true + }, + "shortname": { + "type": "string", + "example": "GPL-2.0-only" + } + } + }, + "models.LicenseMapShortnamesInput": { + "type": "object", + "properties": { + "map": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LicenseMapShortnamesElement" + } + } + } + }, "models.LicenseResponse": { "type": "object", "properties": { @@ -1247,6 +1393,21 @@ const docTemplate = `{ } } }, + "models.LicenseShortnamesInput": { + "type": "object", + "properties": { + "shortnames": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "GPL-2.0-only", + "GPL-2.0-or-later" + ] + } + } + }, "models.LicenseUpdate": { "type": "object", "properties": { diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 393a080..dc833ab 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -537,6 +537,128 @@ } } }, + "/obligation_maps/topic/{topic}/license": { + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Replaces the license list of an obligation topic with the given list in the obligation map.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Change license list", + "operationId": "UpdateLicenseInObligationMap", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation", + "name": "topic", + "in": "path", + "required": true + }, + { + "description": "Shortnames of the licenses to be in map", + "name": "shortnames", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseShortnamesInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationMapResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No license or obligation found.", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Add or remove licenses from obligation map for a given obligation topic", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Add or remove licenses from obligation map", + "operationId": "PatchObligationMap", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation", + "name": "topic", + "in": "path", + "required": true + }, + { + "description": "Shortnames of the licenses with action", + "name": "shortname", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseMapShortnamesInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationMapResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No license or obligation found.", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Failure to insert new maps", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, "/obligations": { "get": { "description": "Get all active obligations from the service", @@ -1223,6 +1345,30 @@ } } }, + "models.LicenseMapShortnamesElement": { + "type": "object", + "properties": { + "add": { + "type": "boolean", + "example": true + }, + "shortname": { + "type": "string", + "example": "GPL-2.0-only" + } + } + }, + "models.LicenseMapShortnamesInput": { + "type": "object", + "properties": { + "map": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LicenseMapShortnamesElement" + } + } + } + }, "models.LicenseResponse": { "type": "object", "properties": { @@ -1241,6 +1387,21 @@ } } }, + "models.LicenseShortnamesInput": { + "type": "object", + "properties": { + "shortnames": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "GPL-2.0-only", + "GPL-2.0-or-later" + ] + } + } + }, "models.LicenseUpdate": { "type": "object", "properties": { @@ -1639,4 +1800,4 @@ "type": "basic" } } -} \ No newline at end of file +} diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 16f0c12..37dbdf5 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -187,6 +187,22 @@ definitions: example: https://opensource.org/licenses/MIT type: string type: object + models.LicenseMapShortnamesElement: + properties: + add: + example: true + type: boolean + shortname: + example: GPL-2.0-only + type: string + type: object + models.LicenseMapShortnamesInput: + properties: + map: + items: + $ref: '#/definitions/models.LicenseMapShortnamesElement' + type: array + type: object models.LicenseResponse: properties: data: @@ -199,6 +215,16 @@ definitions: example: 200 type: integer type: object + models.LicenseShortnamesInput: + properties: + shortnames: + example: + - GPL-2.0-only + - GPL-2.0-or-later + items: + type: string + type: array + type: object models.LicenseUpdate: properties: marydone: @@ -831,6 +857,87 @@ paths: summary: Get maps for an obligation tags: - Obligations + /obligation_maps/topic/{topic}/license: + patch: + consumes: + - application/json + description: Add or remove licenses from obligation map for a given obligation + topic + operationId: PatchObligationMap + parameters: + - description: Topic of the obligation + in: path + name: topic + required: true + type: string + - description: Shortnames of the licenses with action + in: body + name: shortname + required: true + schema: + $ref: '#/definitions/models.LicenseMapShortnamesInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ObligationMapResponse' + "400": + description: Invalid json body + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: No license or obligation found. + schema: + $ref: '#/definitions/models.LicenseError' + "500": + description: Failure to insert new maps + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Add or remove licenses from obligation map + tags: + - Obligations + put: + consumes: + - application/json + description: Replaces the license list of an obligation topic with the given + list in the obligation map. + operationId: UpdateLicenseInObligationMap + parameters: + - description: Topic of the obligation + in: path + name: topic + required: true + type: string + - description: Shortnames of the licenses to be in map + in: body + name: shortnames + required: true + schema: + $ref: '#/definitions/models.LicenseShortnamesInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ObligationMapResponse' + "400": + description: Invalid json body + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: No license or obligation found. + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Change license list + tags: + - Obligations /obligations: get: consumes: diff --git a/go.mod b/go.mod index ce445bd..7a4f9d4 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.2 + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.1 ) @@ -49,10 +50,10 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/tools v0.16.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d48adab..605f5e6 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -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/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -117,15 +117,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -152,10 +154,9 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= diff --git a/pkg/api/api.go b/pkg/api/api.go index c390056..b0d0373 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -95,6 +95,11 @@ func Router() *gin.Engine { obligations.PATCH(":topic", UpdateObligation) obligations.DELETE(":topic", DeleteObligation) } + obMap := authorizedv1.Group("/obligation_maps") + { + obMap.PATCH("topic/:topic/license", PatchObligationMap) + obMap.PUT("topic/:topic/license", UpdateLicenseInObligationMap) + } } // Host the swagger UI at /swagger/index.html diff --git a/pkg/api/obligationmap.go b/pkg/api/obligationmap.go index f0475c8..11e1974 100644 --- a/pkg/api/obligationmap.go +++ b/pkg/api/obligationmap.go @@ -8,8 +8,13 @@ package api import ( "fmt" "net/http" + "strconv" + "strings" "time" + "golang.org/x/exp/slices" + "gorm.io/gorm" + "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" "github.com/gin-gonic/gin" @@ -49,9 +54,7 @@ func GetObligationMapByTopic(c *gin.Context) { return } - query = db.DB.Model(&obMap) - - if err := query.Where(models.ObligationMap{ObligationPk: obligation.Id}).Find(&obMap).Error; err != nil { + if err := getObligationMapsForObligation(obligation.Id, &obMap).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("Obligation map not found for topic '%s'", topic), @@ -85,6 +88,10 @@ func GetObligationMapByTopic(c *gin.Context) { c.JSON(http.StatusOK, res) } +func getObligationMapsForObligation(obligationId int64, obMap *[]models.ObligationMap) *gorm.DB { + return db.DB.Model(&obMap).Where(models.ObligationMap{ObligationPk: obligationId}).Find(&obMap) +} + // GetObligationMapByLicense retrieves obligation maps for given license shortname // // @Summary Get maps for a license @@ -151,3 +158,289 @@ func GetObligationMapByLicense(c *gin.Context) { } c.JSON(http.StatusOK, res) } + +// PatchObligationMap Add or remove licenses from obligation map for a given obligation topic +// +// @Summary Add or remove licenses from obligation map +// @Description Add or remove licenses from obligation map for a given obligation topic +// @Id PatchObligationMap +// @Tags Obligations +// @Accept json +// @Produce json +// @Param topic path string true "Topic of the obligation" +// @Param shortname body models.LicenseMapShortnamesInput true "Shortnames of the licenses with action" +// @Success 200 {object} models.ObligationMapResponse +// @Failure 400 {object} models.LicenseError "Invalid json body" +// @Failure 404 {object} models.LicenseError "No license or obligation found." +// @Failure 500 {object} models.LicenseError "Failure to insert new maps" +// @Security BasicAuth +// @Router /obligation_maps/topic/{topic}/license [patch] +func PatchObligationMap(c *gin.Context) { + var obligation models.Obligation + var obMapInput models.LicenseMapShortnamesInput + var removeLicenseIds []int64 + var insertLicenseIds []int64 + + topic := c.Param("topic") + + if err := c.ShouldBindJSON(&obMapInput); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + query := db.DB.Model(&obligation) + + if err := query.Where(models.Obligation{Topic: topic}).First(&obligation).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: fmt.Sprintf("obligation with topic '%s' not found", topic), + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + + for i := 0; i < len(obMapInput.MapInput); i++ { + var license models.LicenseDB + var obligationMap models.ObligationMap + err := db.DB.Model(&license).Where(&models.LicenseDB{Shortname: obMapInput.MapInput[i].Shortname}).First( + &license).Error + if err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: fmt.Sprintf("license with shortname '%s' not found", obMapInput.MapInput[i].Shortname), + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + if err := db.DB.Model(&obligationMap).Where(&models.ObligationMap{ObligationPk: obligation.Id, + RfPk: license.Id}).First(&obligationMap).Error; err != nil { + // License not in map + if obMapInput.MapInput[i].Add { + // Add to insert slice + insertLicenseIds = append(insertLicenseIds, license.Id) + } + } else { + // License in map + if !obMapInput.MapInput[i].Add { + // Add to remove slice + removeLicenseIds = append(removeLicenseIds, license.Id) + } + } + } + + username := c.GetString("username") + + res := performObligationMapActions(username, obligation, removeLicenseIds, insertLicenseIds) + + c.JSON(http.StatusOK, res) +} + +// UpdateLicenseInObligationMap Update license list of an obligation map +// +// @Summary Change license list +// @Description Replaces the license list of an obligation topic with the given list in the obligation map. +// @Id UpdateLicenseInObligationMap +// @Tags Obligations +// @Accept json +// @Produce json +// @Param topic path string true "Topic of the obligation" +// @Param shortnames body models.LicenseShortnamesInput true "Shortnames of the licenses to be in map" +// @Success 200 {object} models.ObligationMapResponse +// @Failure 400 {object} models.LicenseError "Invalid json body" +// @Failure 404 {object} models.LicenseError "No license or obligation found." +// @Security BasicAuth +// @Router /obligation_maps/topic/{topic}/license [put] +func UpdateLicenseInObligationMap(c *gin.Context) { + var obligation models.Obligation + var obMapInput models.LicenseShortnamesInput + var oldObMaps []models.ObligationMap + var removeLicenseIds []int64 + var insertLicenseIds []int64 + + topic := c.Param("topic") + + if err := c.ShouldBindJSON(&obMapInput); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + obMapInput.Shortnames = slices.Compact(obMapInput.Shortnames) + + query := db.DB.Model(&obligation) + + if err := query.Where(models.Obligation{Topic: topic}).First(&obligation).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: fmt.Sprintf("obligation with topic '%s' not found", topic), + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + + getObligationMapsForObligation(obligation.Id, &oldObMaps) + + for i := 0; i < len(oldObMaps); i++ { + removeLicenseIds = append(removeLicenseIds, oldObMaps[i].RfPk) + } + + for i := 0; i < len(obMapInput.Shortnames); i++ { + var license models.LicenseDB + var obligationMap models.ObligationMap + err := db.DB.Model(&license).Where(&models.LicenseDB{Shortname: obMapInput.Shortnames[i]}).First(&license).Error + if err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: fmt.Sprintf("license with shortname '%s' not found", obMapInput.Shortnames[i]), + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + if err := db.DB.Model(&obligationMap).Where(&models.ObligationMap{ObligationPk: obligation.Id, + RfPk: license.Id}).First(&obligationMap).Error; err != nil { + // License not in map, add to insert slice + insertLicenseIds = append(insertLicenseIds, license.Id) + } + // License in request, remove from removal slice + removeLicenseIds = removeFromSlice(removeLicenseIds, license.Id) + } + + username := c.GetString("username") + + res := performObligationMapActions(username, obligation, removeLicenseIds, insertLicenseIds) + + c.JSON(http.StatusOK, res) +} + +// performObligationMapActions performs the actions for ObligationMap endpoint PATCH and PUT calls. +// It takes the input of obligation which is being modified, list of licenses to be removed and added, +// and the user making the changes. The function computes the changelog and returns the response. +func performObligationMapActions(username string, obligation models.Obligation, removeLicenseIds []int64, + insertLicenseIds []int64) models.ObligationMapResponse { + var oldObMaps []models.ObligationMap + var newObMaps []models.ObligationMap + var removeObMaps []models.ObligationMap + var insertObMaps []models.ObligationMap + + getObligationMapsForObligation(obligation.Id, &oldObMaps) + + for i := 0; i < len(removeLicenseIds); i++ { + deleteItem := models.ObligationMap{ + ObligationPk: obligation.Id, + RfPk: removeLicenseIds[i], + } + // Find the primary key to make delete faster + db.DB.Where(&deleteItem).First(&deleteItem) + removeObMaps = append(removeObMaps, deleteItem) + } + for i := 0; i < len(insertLicenseIds); i++ { + insertObMaps = append(insertObMaps, models.ObligationMap{ + ObligationPk: obligation.Id, + RfPk: insertLicenseIds[i], + }) + } + + if len(removeObMaps) > 0 { + // Bulk delete removeObMaps from DB + db.DB.Delete(&removeObMaps) + } + if len(insertObMaps) > 0 { + // Bulk create insertObMaps in DB + db.DB.Create(&insertObMaps) + } + + getObligationMapsForObligation(obligation.Id, &newObMaps) + + res := models.ObligationMapResponse{ + Data: []models.ObligationMapUser{createObligationMapUser(obligation, newObMaps)}, + Status: http.StatusOK, + Meta: models.PaginationMeta{ + ResourceCount: 1, + }, + } + + var user models.User + db.DB.Where(models.User{Username: username}).First(&user) + audit := models.Audit{ + UserId: user.Id, + TypeId: obligation.Id, + Timestamp: time.Now(), + Type: "obligation_map", + } + + db.DB.Create(&audit) + + change := createObligationMapChangelog(oldObMaps, newObMaps, audit) + db.DB.Create(&change) + return res +} + +// createObligationMapChangelog creates the changelog for the obligation map changes. +func createObligationMapChangelog(oldObMaps []models.ObligationMap, newObMaps []models.ObligationMap, + audit models.Audit) models.ChangeLog { + var oldLicenses []string + var newLicenses []string + + for i := 0; i < len(oldObMaps); i++ { + oldLicenses = append(oldLicenses, strconv.FormatInt(oldObMaps[i].RfPk, 10)) + } + for i := 0; i < len(newObMaps); i++ { + newLicenses = append(newLicenses, strconv.FormatInt(newObMaps[i].RfPk, 10)) + } + + change := models.ChangeLog{ + AuditId: audit.Id, + Field: "RfPk", + OldValue: strings.Join(oldLicenses, ","), + UpdatedValue: strings.Join(newLicenses, ","), + } + return change +} + +// removeFromSlice removes the item from the slice if it exists. +func removeFromSlice[E string | int64](slice []E, item E) []E { + if slices.Contains(slice, item) { + return append(slice[:slices.Index(slice, item)], slice[slices.Index(slice, item)+1:]...) + } + return slice +} + +// createObligationMapUser creates the response data for the obligation map endpoint. +func createObligationMapUser(obligation models.Obligation, obMaps []models.ObligationMap) models.ObligationMapUser { + var shortnameList []string + for i := 0; i < len(obMaps); i++ { + var license models.LicenseDB + licenseQuery := db.DB.Model(&license) + licenseQuery.Where(models.LicenseDB{Id: obMaps[i].RfPk}).First(&license) + shortnameList = append(shortnameList, license.Shortname) + } + return models.ObligationMapUser{ + Topic: obligation.Topic, + Shortnames: shortnameList, + } +} diff --git a/pkg/models/types.go b/pkg/models/types.go index ce5a76b..d539e36 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -262,6 +262,22 @@ type ObligationMapUser struct { Shortnames []string `json:"shortnames" example:"GPL-2.0-only,GPL-2.0-or-later"` } +// LicenseShortnamesInput represents the input format for adding/removing licenses from obligation map. +type LicenseShortnamesInput struct { + Shortnames []string `json:"shortnames" example:"GPL-2.0-only,GPL-2.0-or-later"` +} + +// LicenseMapShortnamesElement Element to hold license shortname and action +type LicenseMapShortnamesElement struct { + Shortname string `json:"shortname" example:"GPL-2.0-only"` + Add bool `json:"add" example:"true"` +} + +// LicenseMapShortnamesInput List of elements to be read as input by API +type LicenseMapShortnamesInput struct { + MapInput []LicenseMapShortnamesElement `json:"map"` +} + // ObligationMapResponse response format for obligation map data. type ObligationMapResponse struct { Status int `json:"status" example:"200"`