GWI Platform Go Challenge#49
Conversation
…d seed data. Add basic authorization mechanism. Add basic tests.
pamanta
left a comment
There was a problem hiding this comment.
Hi @mr-polymath
Thank you very much for taking the time to complete the assignment.
Overall your submission was easy to follow covering plenty of the challenge aspects with a nice touch with swagger, good job!!
I've left some comments for you to have a look when you have the time, no rush :)
| @@ -0,0 +1,21 @@ | |||
| # Use the official Golang image | |||
| FROM golang:latest | |||
There was a problem hiding this comment.
Could using the latest image of golang have any drawbacks? If yes how would you handle the image creation in a production environment
There was a problem hiding this comment.
In production, one should use a specific version of any image and not any tag that changes over time. In this case, using latest may result in new bugs, vulnerabilities and new breaking changes. In addition, behaviour and bug reports will be inconsistent across different clients.
What should have been done in this case is pin the version to the current latest (i.e. 1.25 or 1.25.3) instead of using the "latest" tag.
| The project uses swaggo/swag to generate Swagger documentation. To regenerate the docs run the following command from the repository root: | ||
|
|
||
| ```bash | ||
| $(go env GOPATH)/bin/swag init -g main.go -d cmd/app,internal/api,internal/model --parseInternal -o internal/swagger |
There was a problem hiding this comment.
Can you find of any other way of adding swag without requiring the user to install it manually?
There was a problem hiding this comment.
Running "go run github.com/swaggo/swag/cmd/swag@" instead of "$(go env GOPATH)/bin/swag init" would arguably be better for a more centralised way, it would not require any manual installation and it would be easier for deployment among teams.
| r.Use(gzip.Gzip(gzip.BestCompression)) // Enable gzip compression | ||
|
|
||
| // Swagger UI route | ||
| r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) |
| userId, err := uuid.Parse(userIdFromToken) | ||
| if err != nil { | ||
| c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) |
There was a problem hiding this comment.
Could you please explain the rationale behind returning a StatusNotFound on uuid parsing errors?
There was a problem hiding this comment.
On second thought, this is a 400 BadRequest "invalid ID" situation and not a 404 NotFound "user not found" one. The current error is incorrect, because a parsing error does not indicate a missing resource, but a client error (in this case, an invalid input).
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusNoContent, gin.H{"message": "description updated"}) |
There was a problem hiding this comment.
Do you see anything confusing about this response?
There was a problem hiding this comment.
A 204 NoContent with content is an oxymoron, ergo it should either return 200 StatusOK with (preferably) the resource in its current state rather than a simple message, or not have a body at all (c.JSON(http.StatusNoContent, nil)). I generally prefer 204 for updates, but this depends on the use case and the team's policy.
lkslts64
left a comment
There was a problem hiding this comment.
Hey @mr-polymath, thanks for submitting the assignment! You did really well, showcasing a solid REST API Design with decent code quality 🥇 . You also added some nice extras, like Authentication, Swagger, Docker and tests 💯
I have left a couple of comments for you to check when you find some free time
| } | ||
|
|
||
| // Sorting by q.Order or ID by default, because map does not | ||
| // guarantee the order of iteration |
| type MemoryStore struct { | ||
| mu sync.RWMutex | ||
| // map[userID]map[assetID]Asset | ||
| favourites map[uuid.UUID]map[uuid.UUID]model.Asset |
There was a problem hiding this comment.
Could you briefly explain what are the pros and cons when designing your store with the above nested map versus map[uuid.UUID]model.Asset ?
There was a problem hiding this comment.
map[userID]map[assetID]Asset (what was ultimately used)
Pros:
- O(1) complexity for lookup, update and delete
- O(1) complexity for accessing all user's assets
- Guarantees uniqueness (no duplicates) of user-assets relationship
- Better scalability for many users and assets (although this is an in-memory solution, therefore it might be a moot point)
- Can be easily and quickly paginated for each user
Cons:
- O(n) complexity when getting all assets for a user
- Technically a bit more complicated than other solutions
- Needs ordering (maps do not guarantee order of iteration)
- Slight memory overhead due to additional key
- No easy way to lookup a specific asset
map[assetID]Asset (I assume the question was not about map[userID]Asset, as that conflicts with the challenge's requirements)
Pros:
- O(1) complexity for a specific asset lookup
- Simpler
- Less memory overhead
Cons:
- O(n) complexity when getting all assets for a user (asset needs no store userID internally and would need to iterate through all assets and filter by userID)
- Unclear data separation between users and their assets
- Needs ordering
- No efficient pagination (every query requires iterating through assets and filtering by user every time)
- Does not scale for many users
map[userID][]Asset (a map of slices, another contender, my initial implementation and what I first thought/read you asked)
Pros:
- O(1) complexity for getting all assets for a user
- Simpler than the nested map solution
- Does not need ordering - slices preserve order
Cons:
- O(n) complexity for lookup, update and delete
- Possibility of duplicate entries in the slice (it is not enforced by a key, like in maps)
- Deletion requires creating a new slice
- Does not scale for many assets
| return | ||
| } | ||
|
|
||
| var req patchReq |
There was a problem hiding this comment.
Currently, the path request body only accepts the "Description" attribute, which is fine. If you were to add support for the "Data" attribute, can you explain, in a high-level way, how you would implement that? For example, would the Asset.Data type remain any or would you change that?
There was a problem hiding this comment.
-
I decided to use "Data any" mainly for simplicity. The better way is to provide a strongly-typed data type. This can be achieved with a struct (AssetData, for example) that contains pointers to each asset data (*ChartData, *InsightData and *AudienceData, in this case). This gives type safety, predictable JSON schemas, and better Swagger docs and validation.
-
Another change would be extending the PatchFavourite to receive a partial update body, with description and data as optional parameters (instead of adding each argument separately to the function). This would be represented as a struct with a "Description *string" and "Data *AssetData" fields.
-
As a minor point, if both Description and Data are nil (as in no changes were provided), the endpoint would return a 400 BadRequest status. If either/both are provided, assuming no other errors occurred, then the endpoint would return 200 Ok/204 NoContent, regardless of whether actual changes occurred or not. Although not providing data and providing unchanged data would be syntactically correct and have the same end result, semantically the 1st case would have no changes to apply, therefore no successful update can possibly occur.
No description provided.