Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to use router in backend implementations to generate URLs to frontend resources? #6

Open
dmitshur opened this issue Dec 5, 2015 · 4 comments

Comments

@dmitshur
Copy link

dmitshur commented Dec 5, 2015

Hi,

I have a question about this package. I've looked through the README and godoc.org (for the original version) and couldn't find a conclusive answer.

Suppose I have a web project that separates the frontend code (including display stuff, html, css, and http request routing and handling) from the backend service implementation (expressed as a Go interface, implemented by some concrete type).

In some situations I'd like to be able to generate URLs to frontend resources from the backend.

Is it possible to reuse the router for that purpose? For example, suppose I want to generate a relative URL to the "hello" resource named "foobar".

Can I do that in some way using the router? For example, maybe:

var url *url.URL = something ... router.HelloRoute ... {router.HelloRoute.Name: "foobar"} ...
fmt.Println(url.String())

// Output:
// /hello/foobar

Is this supported right now, and if not, is it support planned? Or do you see a better way to resolve the scenario I described above.

My goal is to avoid duplicating the routing logic in the frontend and backend, so that changing the hello route from /hello/:name to /hi/:name should only have to be done in one place, and it'd affect both frontend and backend.

Thanks! I like the other decisions made by this library, like keeping it simple, using context.Context, etc.

@dmitshur dmitshur changed the title Is it possible to use router in backend implementations to generate URLs to resources? Is it possible to use router in backend implementations to generate URLs to frontend resources? Dec 5, 2015
@zenazn
Copy link
Member

zenazn commented Dec 6, 2015

Funny you should mention this--there's a proposal for URL reversal in pat in #4 (that I promise I'll get to soon!). The idea would be that the router wouldn't be involved at all (I'd like to keep as much out of the top-level goji.io package as possible), but individual implementations of the Pattern interface might allow reversal if doing so is feasible.

Would this satisfy your use case?

As a separate aside, I'm curious how often you change the format of existing routes. The "make it easy to change" argument is quite familiar to me, but I'll admit I've never done it myself, since in my experience doing so almost always breaks existing clients. That being said, the question comes up a lot (and came up a lot for the previous version of Goji as well), and I suspect I may be missing something; any insight you have would be greatly appreciated!

@dmitshur
Copy link
Author

dmitshur commented Dec 6, 2015

Funny you should mention this--there's a proposal for URL reversal in pat in #4

Would this satisfy your use case?

Hmm, maybe.

Would it look something like this?

var (
    HelloRoute  = pat.Get("/hello/:name")
    ThreadRoute = pat.Get("/threads/:id")
)

mux.HandleFuncC(HelloRoute, hello)

u := HelloRoute.URLPath(map[pattern.Variable]string{HelloRoute.Name: "foobar"})

I can't say for sure until I try it out for real.

As a separate aside, I'm curious how often you change the format of existing routes. The "make it easy to change" argument is quite familiar to me, but I'll admit I've never done it myself, since in my experience doing so almost always breaks existing clients.

How often? So far, very infrequently. However, the primary reason for that is precisely because I know that there may be things broken as a result. It's hard to be sure I didn't forget to update all instances that hardcode the paths somewhere.

However, in most cases I control all clients and everything is written in Go. There is an incredible tool at my disposal that can help catch things I forgot to update, that is the Go compiler and its static type system. I'd like to start using it for this task, and doing that will enable me to change routes effortlessly - if or when I have that need.

@zenazn
Copy link
Member

zenazn commented Dec 6, 2015

It'll probably look closer to

u := HelloRoute.URLPath(map[pattern.Variable]string{"name": "foobar"})

(I don't know how to make something like HelloRoute.Name work without codegen)

And understood RE: the desire for safety / leaning on the type system for correctness.

@dmitshur
Copy link
Author

dmitshur commented Dec 6, 2015

Gotcha. I'll think about what can be done about pattern variables, because using "name" string means the Go compiler will not give me an error if I were to change the name of that variable and forget to update it somewhere. That defeats a part of the goal. I know it's not an easy problem to solve, but I'm interested in thinking more about it.

I can also point you to a recent real-world example of where I ran into this, which can help explain my motivation.

When working on a tracker app, in one of the WIP commits I had this hardcoded URL path with the comment TODO: Use router(s).:

// TODO: Use router(s).
path := fmt.Sprintf("/%s/.tracker/%d", repo.URI, id)
fragment := fmt.Sprintf("comment-%d", commentID)

The entire value of path was duplicating the routing logic that our existing gorilla/mux routers, and I wanted to avoid that. I ended up having this code to use routers, which makes the solution type safe and more reliable, but I'm not a fan of how verbose it became:

// Use Sourcegraph app router for repo app path and Tracker app router for the rest.
trackerURL, err := sgrouter.Rel.Get(sgrouter.RepoAppFrame).URLPath(
    "Repo", repo.URI,
    "App", s.appName,
    "AppPath", "",
)
if err != nil {
    return fmt.Errorf("failed to produce relative URL for tracker app: %v", err)
}
issueURL, err := trackerrouter.Router.Get(trackerrouter.Issue).URLPath("id", formatUint64(issueID))
if err != nil {
    return fmt.Errorf("failed to produce relative URL for issue: %v", err)
}
u := &url.URL{
    Path:     path.Join(trackerURL.Path, issueURL.Path),
    Fragment: fragment,
}
htmlURL := template.URL(conf.AppURL(s.appCtx).ResolveReference(u).String())

At some point I tried to comment out "AppPath", "", from the URLPath method call, and it compiled, but didn't work at runtime. I'd like to find a way to push those kind of error checks to be at compile-time.

Also note that the value of fragment part of the URL was not generated with the router, but I'd like for it to be as well.

Hopefully this real-world example can be helpful in showing my motivation.

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

No branches or pull requests

2 participants