Skip to content

GWI Platform Go Challenge#49

Open
mr-polymath wants to merge 3 commits into
GlobalWebIndex:mainfrom
mr-polymath:main
Open

GWI Platform Go Challenge#49
mr-polymath wants to merge 3 commits into
GlobalWebIndex:mainfrom
mr-polymath:main

Conversation

@mr-polymath

Copy link
Copy Markdown

No description provided.

@pamanta pamanta left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Comment thread Dockerfile
@@ -0,0 +1,21 @@
# Use the official Golang image
FROM golang:latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could using the latest image of golang have any drawbacks? If yes how would you handle the image creation in a production environment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread README.md
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you find of any other way of adding swag without requiring the user to install it manually?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/app/main.go
r.Use(gzip.Gzip(gzip.BestCompression)) // Enable gzip compression

// Swagger UI route
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Comment thread internal/api/handler.go
Comment on lines +245 to +247
userId, err := uuid.Parse(userIdFromToken)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please explain the rationale behind returning a StatusNotFound on uuid parsing errors?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread internal/api/handler.go
return
}

c.JSON(http.StatusNoContent, gin.H{"message": "description updated"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see anything confusing about this response?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lkslts64 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ☺️

Comment thread internal/db/memory.go
}

// Sorting by q.Order or ID by default, because map does not
// guarantee the order of iteration

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job 💯

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Comment thread internal/db/memory.go
type MemoryStore struct {
mu sync.RWMutex
// map[userID]map[assetID]Asset
favourites map[uuid.UUID]map[uuid.UUID]model.Asset

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread internal/api/handler.go
return
}

var req patchReq

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

  2. 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.

  3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants