Skip to content

Conversation

dpordomingo
Copy link
Contributor

@dpordomingo dpordomingo commented Jan 18, 2018

partially solves #3

This PR will create a backend api serving mocks, using the requirements defined by #3

If approved, the things that will be done in the next PRs will be:

  • using DB stuff,
  • tests,
  • improve error handling and error messages,
  • using user from Auth

@bzz bzz added the review label Jan 18, 2018
return nil
}

type Identifable interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

typo? identifiable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ouch! many thanks

@bzz
Copy link
Contributor

bzz commented Jan 19, 2018

Nice!

A bit confused though - what is the reason to have all the code under /cmd/ package?

Usually, it is reserved for executable things, like server of CLI, etc. (see borges/cli). Everything else could belong to a better-named package under /server/.

@smacker
Copy link
Contributor

smacker commented Jan 19, 2018

Could you please not merge it yet. I want to take a look. I'll do it a bit later today. Thanks.

@dpordomingo
Copy link
Contributor Author

Sure!
I think it's a good idea to avoid merging things before they're fully reviewed by all of us.

Copy link
Contributor

@smacker smacker left a comment

Choose a reason for hiding this comment

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

Good job. But I have comments. Some of them are actually applicable to many similar lines, but I didn't copy-past them to avoid bloating of PR.

"github.com/go-chi/chi"
"github.com/go-chi/render"

"github.com/src-d/code-annotation/server/cmd/code-annotation/serializer"
Copy link
Contributor

Choose a reason for hiding this comment

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

do you think it's better to move it out of cmd? It's a common pattern in go to keep only entry point in cmd folder.
Please let me know if you need more information about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!!!
That aligns quite well with Alex comment
I tried to follow the initial skeleton and I might misunderstand it.
I'd propose the following #16 (comment)

"github.com/src-d/code-annotation/server/repository"
)

func HandleGetAssignmentsForUserExperiment(w http.ResponseWriter, r *http.Request) {
Copy link
Contributor

Choose a reason for hiding this comment

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

do you think we could follow https://github.com/golang/go/wiki/CodeReviewComments#package-names here?
The package has name handler. So I would suggest handler.GetAssignmentsByUser. I think we don't really need experiment in the name because looks like assignments always belong to experiment. Feel free to suggest better name!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd sugest handler.HandleInHandlerPackageThatGetAssignmentsByUserExperiment
:trollface:

You're totally right.

I only disagree in the Experiment suffix, because there will be many experiments; that's because handled URL is /experiments/{experimentId}/assignments.

func HandleGetAssignmentsForUserExperiment(w http.ResponseWriter, r *http.Request) {
experimentID, err := strconv.Atoi(chi.URLParam(r, "experimentId"))
if err != nil {
render.Render(w, r, serializer.NewErrors(err))
Copy link
Contributor

Choose a reason for hiding this comment

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

do you think we can provide better error message here? Currently, it's strconv.Atoi: parsing "qwe": invalid syntax. I would suggest add information that we can't parse experimentId not a random string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure thing!
That's because I left as "pending" in the PR desc:

improve error handling and error messages

I just tried to postpone the error messages for the next iteration, so it wouldn't block the API.
If you agree, we'd merge without this "feature".

return
}

assignments, err := repository.GetAssignmentsFor(userID, experimentID)
Copy link
Contributor

Choose a reason for hiding this comment

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

As I remember here we create all assignments. How do you think is it better to put creation logic here or in a service? And keep repository as a slim interface to DB?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking in:

assignments, err := repository.GetAssignmentsFor(userID, experimentID)
if err.is(NO_ASSIGNMENTS_INITIALIZED) {
    assignments, err = repository.CreateAssignmentsFor(userID, experimentID)
} 

I agree on putting business logic in service, instead of in repository... but should we create a new service package for only these 3 lines that could be in the handler.GetAssignmentsForUserExperiment ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would put it in handler as you posted snippet above for now. Later if we have more cases we can refactor it to service.

return
}

var assignment serializer.Assignment
Copy link
Contributor

Choose a reason for hiding this comment

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

I usually try to keep structures close to the place where they are used (in the same package and as a private struct) if they are used only once. What is your opinion about it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that handlers and serializers should be separated.

Copy link
Contributor

Choose a reason for hiding this comment

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

could you please explain why? How are you going to use serializers beside in handlers? Because in Go usually, you export functions/types if you consider them as your API.
And it's a good idea to keep your API as small as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Packages also help us to choose good names.

If we now care about the exposed API, it's because we're taking care of our external users, but this repo does not contain a library to be used for third parties but a repo containing a bunch of things like a web app, tools for DB handling, an API...

If we still think that a curated external API is needed here, we could move that internal stuff to internal packages and keep the benefits of both sides.

Copy link
Contributor

Choose a reason for hiding this comment

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

I still don't like to have a package for only 1 particular consumer. But I can live with it.

return
}

//TODO: use chi logger
Copy link
Contributor

Choose a reason for hiding this comment

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

can we use logrus?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd use chi default through chi/middleware/Logger, whose default seems to be STDOUT and STDERR, but also lets us use Logrus or others.

I'd prefer to consider the Logging lib to use in a separate issue since we're using three big logging libraries in different of our biggest projects

  • log → go-git...
  • github.com/inconshreveable/log15 → borges, rovers, framework...
  • github.com/sirupsen/logrus → go-compose-installer

Copy link
Contributor

Choose a reason for hiding this comment

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

chi/middleware/Logger the name of the package says you it's not supposed to be used outside of middleware. There is no problem to use middleware with logrus: https://github.com/go-chi/chi/blob/master/_examples/logging/main.go

log15 is also fine for me, can we votelogrus vslog15 then?
for go-git using standard log makes sense. it's library, not application.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't test it yet, but if I understood it the Logger can be retrieved (and used) from the inside of the handlers;

type printfer interface{
  func Printf(format string, v ...interface{})
}

and then

logger := r.Context().Value(middleware.LogEntryCtxKey).(printfer)
logger.Printf("whatever")

About the log library to use, I think we should consider it as a team decision, considering the company first rule; I mean, imho we should use the same logger in all our projects, and it should be the one that is "the most wanted" by engineering. What do you think about that decision @bzz ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use Logrus for consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's what I was trying to find: Choose one log library for Go and implement on all the projects, many thanks for pointing it out !!

import (
"net/http"

"github.com/go-chi/render"
Copy link
Contributor

Choose a reason for hiding this comment

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

render package is quite complex. The main advantage of it, you can handle different types of input/output. Like json/xml for example. But I'm not sure we really need it in this project.
Are you open to considering simpler solution?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was quite easy to implement in our case, but I'd reconsider it for sure, and ping you when done 🗡️ Thanks for being nitpicker !!!

}

type ResponseMany struct {
Data []Identifiable `json:"data"`
Copy link
Contributor

Choose a reason for hiding this comment

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

do you think it's possible we will render something that doesn't have id?
I personally prefer to add constraints only when they are really necessary.
please consider this comment only as an open discussion, not action request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You may be right. I'll reconsider it and I'll ping you with the answer. Thanks for suggesting it!

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't advocate for this particular code, just another way how to do it for you to consider: #22

I also find empty return approach error prone. In my previous project, I myself fixed like 5 bugs about it before we moved to non-empty return. And reviewed about 5 more fixed by other developers. I mean bugs that we found in production, not during CR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with the 1st paragraph.

I'm lost in "empty return approach". I could not find any occurrence of empty word in this PR. Could you give me a clue?

Copy link
Contributor

Choose a reason for hiding this comment

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

func Handler(w, r) {
  if err != nil {
    write(w)
    return // empty return
  }
  write(w)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what Maxim means is that it's a good and less error-prone to always return something like i.e https://github.com/src-d/code-annotation/pull/22/files#diff-3e10846b2816b527e6724efedecbb964R15

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a global assertion, I agree with it... but in the context of chi handlers, the expected ones must have the signature http.HandlerFunc, that is "empty return"
In the code you linked, the empty return handler is deeper, but it is

If you're talking about status, errors and so on, I agree, but it's not related to the "empty return" funcs

Copy link
Contributor

Choose a reason for hiding this comment

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

it's not chi handlers. It's net/http handlers. Of course there is empty return on top level. But I can't imagine how you can make mistake there. Everything inside won't compile without proper return.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

chi.Router.Get(pattern string, h http.HandlerFunc) requires a http.HandlerFunc (an empty return func). It does not matter what you put as h, that it'll be an empty return func.

Again: I agree on the status, errors and so on, but it's a different thing that I already confirmed it will be added to the server.

@@ -0,0 +1,54 @@
package model

type User struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

we defined this model in schema task, but it doesn't have all necessary field. For example avatar url.
I would also like to abstract from github in model. login and username fields are better in my opinion. Then we can add other providers later. Google for example. Or allow registering user. If you use abstract model consumers of model don't need to know how user was created.
Please share your thought!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just tried to follow our specs because I was told that other log-in providers are not expected in the middle term. (We even considered using the gh-user-name as the User.ID) Anyhow, I'd consider https://github.com/src-d/code-annotation/pull/22/files#diff-c318b873cebb799af6f409abe8089672 for sure.

About storing the avatar... I'd like to confirm it with @src-d/product, because in our former web apps they requested us to do not store it, but loading it from gh (so it would be always updated) htps://avatars2.githubusercontent.com/u/{gh-user-id}

To do so, let us know if you can retrieve the gh-user-id from GH, or if we should store it in the DB.

Copy link
Contributor

Choose a reason for hiding this comment

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

Github return https://avatars0.githubusercontent.com/u/406916?v=4 as avatar url. I suggest to store it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a business decision.

@bzz should we keep the user avatar in sync with the real one?

  • in sync: showing whatever avatar GH shows under htps://avatars2.githubusercontent.com/u/{gh-user-id}
  • outdated: showing a stored version when the user first logged in

Copy link
Contributor

@bzz bzz Jan 19, 2018

Choose a reason for hiding this comment

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

It's totally fine, either not to have avatar at all, or to show an outdated on.

On the side-note: personally, I would prefer us merging and improving already working things in subsequent small PRs rather then arguing about abstract things in single long PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

406916 is a userId, so the image itself is always fresh.

type Answer string

const (
Yes Answer = "yes"
Copy link
Contributor

Choose a reason for hiding this comment

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

On one hand yes, no, maybe sounds common enough. On another hand, I'm not 100% sure it's the best options for our tool.
I remember you spent time trying other tools. Could you please remind me what do they use as names for action?
It's very basic part of the tool, I just want to do it right. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That values should not be exposed in the UI, never-ever.

TL;DR
(this answer is based in #2 (comment) but feel free to keep answering me if the conclusions below does not satisfy you)

MTurk

In MTurk, the Assignment tracks the Answer of a HIT, as we do, but the Answer of an Assignment is not defined by the platform itself but by the Requester.

In MTurk the Answer schema is defined by the Project itself,
An MTurk Answer schema could be whatever nested thing, a quite simple could be:
{name:string, age:int, gender:bool, desc:string, croppedImage:[]blob}
but we don't have that concept because we were requested:

  • to use a single Project type, whose Answer schema would be: {answer:int[1-4]},
  • to avoid unnecessary normalization;

because of that requirements, we decide by #2 (comment) :

  • to have Experiment instead of Project,
  • to avoid Answer model, and just embed it in the Assignment

Considering that our accepted answers are an enum of size 4, we just use that "strings" instead of using int[1-4].
Because of that:

  • the internal value of the Assignment.Answer should not be exposed to the UI.
  • the Front should have a mapping ( DB/API ⇄ UI ) between the enum values used in the DB/API, and labels that the user will see.

Supervise.ly

Supervise.ly focuses on image labeling (so they does not have Projects neither) and their answers are class oriented. Because of the same reason than in MTurk, the class names are always defined by the Requester, and they can be whatever random word or group of chars (example: Car, Dog, Cake, UFA4E4AS...

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah. It's only internal name. But I still want to have it "correct".
Looks like yes, no, maybe, skip are ok if we consider it as enum 🎉
Good to go!

@dpordomingo
Copy link
Contributor Author

answering @bzz question #16 (comment)
I tried to follow the initial skeleton but you're right, I'll ping you when it had been reordered.
It would be:

/
  public # web stuff
  src # web sources
  cli
    import/import.go
    export/export.go
    server/server.go
    examples/import/example.sql # it may be moved to of /cli/import/examples/example.sql
  server
    handler
    serializer
    model
    repository
    dbutil # it may be moved outside of /server (to the root path)

Comment with 👍 if you agree, or provide a new suggestion 😉

@bzz bzz removed the review label Jan 19, 2018
@bzz
Copy link
Contributor

bzz commented Jan 19, 2018

@smacker could you summarize only the main points you see blocking us from merging this PR in a check-list form?

Feel free to update this one:

I think that would help us to decide with the order of merging things, i.e this, then #22 or other way round.

Update: was updated by Maxim.

@dpordomingo does it make sense to you, to fix this asap and merge then?

corsOptions := cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "OPTIONS"},
AllowedHeaders: []string{"Location", "Authorization"},
Copy link
Contributor

Choose a reason for hiding this comment

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

we also need content-type header

Copy link
Contributor Author

@dpordomingo dpordomingo Jan 23, 2018

Choose a reason for hiding this comment

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

addressed by 28c54c4

// protected handlers
r.Route("/api", func(r chi.Router) {
r.Use(jwt.Middleware)
r.Route("/experiments/{experimentId}", func(r chi.Router) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should be inside api and protected

Copy link
Contributor Author

@dpordomingo dpordomingo Jan 23, 2018

Choose a reason for hiding this comment

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

addressed by 28c54c4

r.Get("/me", handler.Me(userRepo))
})

r.Get("/login", handler.Login(oauth))
Copy link
Contributor

Choose a reason for hiding this comment

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

Recoverer and logger middlewares must be used for all routers

Copy link
Contributor Author

@dpordomingo dpordomingo Jan 23, 2018

Choose a reason for hiding this comment

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

addressed by 28c54c4


// OAuthCallback makes exchange with oauth provider, gets&creates user and redirects to index page with JWT token
func OAuthCallback(oAuth *service.OAuth, jwt *service.JWT, userRepo *repository.Users, uiDomain string) http.HandlerFunc {
logger := service.NewLogger()
Copy link
Contributor

Choose a reason for hiding this comment

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

should it be a dependency? for example in tests you want to use another logger

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I started making the logger injectable/configurable, so you could define which one to use in different scenarios.
But I rollbacked to remove initial complexity.
Do we want that for this first iteration?

Copy link
Contributor

Choose a reason for hiding this comment

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

where is a complexity to change

func OAuthCallback() {
  logger := service.NewLogger()
}

to

func OAuthCallback(logger *logrus.FieldLogger) {
}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is now:

func OAuthCallback(oAuth *service.OAuth, jwt *service.JWT, userRepo *repository.Users, uiDomain string) http.HandlerFunc {

I didn't want to add more params to some handlers, but I can do so for sure.
I saw that chi/logger uses the context to store the logger, what could be another approach.
Which one would be your option?

  • a: one parammeter more,
  • b: using the context as chi does

Copy link
Contributor

Choose a reason for hiding this comment

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

the easiest way to just add one more param. The better way is to convert it to structure.
for logger it's okay to store it in context (but you will need a helper, which returns "default" logger if there is no logger in context), especially if you want to have request scoped logger (and I want).

But remember Context.Value should inform, not control. Here is a good article about it: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39

Copy link
Contributor Author

@dpordomingo dpordomingo Jan 23, 2018

Choose a reason for hiding this comment

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

so a, and lets proceed with b in the next iteration

Copy link
Contributor Author

@dpordomingo dpordomingo Jan 23, 2018

Choose a reason for hiding this comment

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

addressed by 28c54c4

return append(ms, Appender)
}

func Appender(next http.Handler) http.Handler {
Copy link
Contributor

Choose a reason for hiding this comment

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

we definitely need comments here. It isn't obvious what Appender does

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right.
And the name is not clear at all.
Options:

  • now,
  • merge and document.

Anyhow, I planned to add it to the login/callback so all errors, logging... will be homogeneous

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to document it now. Because I don't understand it completely even for review.
Like what would happen if I use w as a normal writer. Looks like it will just fail even if you implement http.ResponseWriter methods instead of current panic.
Is it normal behavior? Is it a bug?
Without documentation I don't know.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you're using that middleware, the w you receive won't accept writes.
I'm writing a couple of lines with doc; I'll ping you in a few mins when ready :D

Copy link
Contributor

Choose a reason for hiding this comment

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

then it's completely incorrect code.
handler accepts http.ResponseWriter. But it isn't http.ResponseWriter anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

functionality dropped


experimentID, err := strconv.Atoi(chi.URLParam(r, "experimentId"))
if err != nil {
postponed.AddError(&serializer.Error{500, "Wrong format in experiment ID sent", err.Error()})
Copy link
Contributor

Choose a reason for hiding this comment

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

http.StatusInternalError

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I planned to review all errors codes and texts to use constants and make them more clear.
Should I do it before merging? Just say so 💃

Copy link
Contributor

Choose a reason for hiding this comment

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

it's a very simple change. Why postpone?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't want 500 for everything because there could be any other with more sense. But yes, that enhanced can be postponed too.
Adding http constants now


experiment, err := repository.GetExperimentById(experimentID)
if err != nil {
postponed.AddError(&serializer.Error{404, "No experiment found", err.Error()})
Copy link
Contributor

Choose a reason for hiding this comment

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

http.StatusNotFound

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ditto


func GetAssignmentsForUserExperiment(w http.ResponseWriter, r *http.Request) {
//TODO: !ok
postponed, _ := w.(middleware.PostponedResponse)
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the point to "postpone" response? I checked handlers and I don't see where you might need it. Every time you return you can create normal response from the line just above that return.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the postponed Responses are the way to avoid writing things randomly; that way the handlers push the results to the postponed Response, and at the end of the flow it is processed and properly serialized, the errors are checked...

Copy link
Contributor

Choose a reason for hiding this comment

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

you are talking about transformation. Not postponing.

Postponing is something like

func handler(w) {
  writeSomethingTo(w)
  // do long computation maybe in goroutine
  writeMoreTo(w)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Response is written after the handler is run

Copy link
Contributor

Choose a reason for hiding this comment

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

I get it. I'm saying the name is confusing. If you want to keep using this approach (I don't) the name like Responser is much better. Because it's interface and consumer doesn't need to know how it works internally (does it postpone anything or not)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

functionality dropped

})
}

type postponed struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

wrapping http.ResponseWriter isn't such an easy task. You can read about it more here for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to emulate the same flow that chi/middleware/compress and chi/middleware/logger follows. Both wrap it to ensure the response is created in a controlled and homogeneous way.

Thanks for the resource, I'll read a bit more to consider unhandled circumstances.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

functionality dropped

@dpordomingo
Copy link
Contributor Author

Done, here is the summary:

I'll review the constants for status because there could be some missing

@dpordomingo
Copy link
Contributor Author

PS:
Many thanks to all of you for being nitpicking 🗡️

middleware.Recoverer,
cors.New(corsOptions).Handler,
middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger}),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

better write as in documentation:

r := chi.NewRouter()
r.Use(middleware.Recoverer)
r.Use(cors.New(corsOptions).Handler)
r.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger}))

r.Get("/login", GetHandler(handler.Login(oauth)))
r.Get("/oauth-callback", GetHandler(handler.OAuthCallback(oauth, jwt, userRepo, conf.UIDomain, logger)))

r.Route("/api", func(r chi.Router) {
  r.Use(jwt.Middleware)
  ...
})

not because current code is wrong, but because it isn't that easy to understand.
Like I need to scan all code below this line for ms.

If you want to exclude static (though I don't see why, I think it should be logged also and FrontendStatics may panic also). You can wrap it in another r.Group. But r.Use, r.Use is much more readable than array.

)

func GetAssignmentsForUserExperiment() RequestProcessFunc {
return func(r *http.Request) (resp *serializer.Response, errs []*serializer.Error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters

also you lose all the profit of go type checking.

consider code:

func(r *http.Request) (resp *serializer.Response, errs []*serializer.Error) {
  ...a lot of code...
  if somecheck {
    resp = postResponse()
  }
  ...a lot of code...
  resp = getResponse()
  return
}

code above compiles and produces wrong response.

this signature and always return new value avoids the problem:

func(r *http.Request) (*serializer.Response, []*serializer.Error) {

"github.com/src-d/code-annotation/server/model"

jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm sure my editor will fix it back as soon as I open this file.
I'm too lazy to look for a link right now, but it's good practice to use named import if name of package != import path.
With jwt-go it's not that bad. But sometimes names are much different and it's just difficult to understand where this package came from.

return func(r *http.Request) (resp *serializer.Response, errs []*serializer.Error) {
url := oAuth.MakeAuthURL()
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
resp = serializer.NewTemporalRedirect(url)
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the point of using multiple layers of logic for it if you actually just do http.Redirect anyway?

status := http.StatusOK

if len(errs) > 0 {
status = http.StatusInternalServerError
Copy link
Contributor

Choose a reason for hiding this comment

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

something is wrong here.
if handler returns error not found the status should be not found not internal server error

status = http.StatusInternalServerError
response = &serializer.Response{Status: status, Errors: errs}
for _, e := range errs {
//TODO: Not all errors should be logged as .Error()
Copy link
Contributor

Choose a reason for hiding this comment

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

4xx statuses are client errors, you don't need to log them
5xx statuses are server errors, we need to log them

filepath := "build" + r.URL.Path
if _, err := os.Stat(filepath); err == nil {
http.ServeFile(w, r, filepath)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

and here we go. empty return = mistake.

it was easy to spot in a handler with 5 lines of code.
But in with more code it's' more difficult.

That's why I'm so aggressively against of empty return when we don't have to use them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem was caused because the return from the original sourced dissapeared (:unamused:) when the original code was moved to the handler package.

I don't see how could it be avoided by a qualified return; example, given the following code:

func () string {
    if (cond) {
        doSomething()
        return "true"
    }
    return "false"
}

the one above will compile too, and has the same problem.

func () string {
    if (cond) {
        doSomething()
    }
    return "false"
}

Copy link
Contributor

Choose a reason for hiding this comment

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

func FrontendStatics(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, func() string {
        filepath := "build" + r.URL.Path
        if _, err := os.Stat(filepath); err == nil {
            // you can't write filepath without return, it won't compile
            return filepath
        }
        return "build/index.html"
    }())
}

but I don't suggest to change this handler. It's small, so no need to do it more complex. But for other handlers it makes sense.

P.S. Of course you can always make a mistake, just the chance is lower with non-empty returns.

return func(r *http.Request) (resp *serializer.Response, errs []*serializer.Error) {
experimentID, err := strconv.Atoi(chi.URLParam(r, "experimentId"))
if err != nil {
errs = append(errs, &serializer.Error{500, "Wrong format in experiment ID sent", err.Error()})
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry I don't remember in which document it's described. But go vet won't allow this code with warning like composite literal uses unkeyed fields https://golang.org/cmd/vet/#hdr-Unkeyed_composite_literals

And it makes a lot of sense!
If you change the order of fields (last 2) in your struct - code still compiles but works incorrectly.
Also it will be very painful to add a new optional field.

return assignments, nil
}

func UpdateAssignment(assignmentId int, assignment *model.Assignment) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like you missed my comment before.
What behavior should I expect from this method if assignmentId != assignment.ID?
I don't have a correct answer.
That's why I suggested to change func signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From the specs #3 (comment), the assignment request comes with no ID, so it is not considered
Same happens with answer.PairID: it is ignored.
It will be ensured in the tests too.

Anyhow, I'll rollback to

func UpdateAssignment(assignmentId int, answer string, duration int) error

That is more clear and guarantee our requirements by design

Copy link
Contributor

Choose a reason for hiding this comment

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

we need to add id to an assignment. Because FE needs to update it. RESTful way to do it is to call PUT .../assignments/{id}

Description string `json:"description"`
}

func NewExperimentResponse(e *model.Experiment) *Response {
Copy link
Contributor

Choose a reason for hiding this comment

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

you just broke name convention.
If a package has public ExperimentResponse structure, NewExperimentResponse should return it. Not Response.
ReturningResponse will make sense if ExperimentResponse is private.

@dpordomingo
Copy link
Contributor Author

superseded by #37

@dpordomingo dpordomingo reopened this Jan 30, 2018
@dpordomingo dpordomingo mentioned this pull request Jan 30, 2018
@bzz bzz mentioned this pull request Feb 6, 2018
2 tasks
@dpordomingo dpordomingo deleted the api branch February 9, 2018 18:28
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.

4 participants