Skip to content
This repository has been archived by the owner on Feb 9, 2022. It is now read-only.

A Simpler Library? #2

Open
moonglum opened this issue Dec 22, 2015 · 12 comments
Open

A Simpler Library? #2

moonglum opened this issue Dec 22, 2015 · 12 comments

Comments

@moonglum
Copy link
Owner

Shigeru can change entirely as long as it has not hit 1.0, there's no part of it that can't be changed apart from the goal (provide a simple and clever uri_for helper). So this ticket can be used to discuss any idea how this could be done differently!

@moonglum
Copy link
Owner Author

URI Templates are very powerful. But would something less powerful do the trick as well? There are different kinds of parameters in an URI:

  • Path Parameters: Parts of URIs can be dynamic and therefore be a parameter
  • Query Parameters (?a=123): We use the common form that is for example used by HTTP Get Forms
  • Fragment identifiers (#fragment)

There can only be one fragment identifier, it does not have a name. Therefore we name this parameter fragment. We further assume that you only treat entire segments of your URI as a parameter and that you declare them by name. Therefore all path parameters have to be provided to the helper (otherwise it will complain), and all additional parameters will be treated as query parameters. Using this method, we only need to declare path fragments as being a parameter. Everything else would work automatically. It could be done like this:

routes.define :user, '/users/:id'

# then:
routes.uri_for :user, id: 12 #=> /users/12
routes.uri_for :user, id: 12, fragment: 'profile' #=> /users/12#profile
routes.uri_for :user, id: 12, x: 12 #=> /users/12?x=12
routes.uri_for :user, id: 12, x: [12, 13] #=> /users/12?x=12&x=13

It is not as powerful as what Shigeru provides now, but it is probably covering most cases. It would also make it easy to require path parameters while leaving query parameters and fragment identifiers optional (see #3). It however does not allow to whitelist query parameters.

@moonglum
Copy link
Owner Author

@frodsan As I can't mention you on Twitter for some reason (have you left Twitter?), let me ping you here 😄

@tonchis
Copy link

tonchis commented Dec 22, 2015

Well hello there @moonglum! I like this idea. Let me jump right in with a comment.

These two things look like they're the same, but are not:

routes.uri_for(:user, id: 13)

vs

routes.uri_for(:users, name: 'alice', country: 'argentina')

The downside I see here is that the interface is the same, just passing in a hash, but the outcome is different; the first example builds "/user/13", the second one "/users?name=alice&country=argentina" where there's a query string.

The only way I can be aware of this is by going to the definition of the routes and looking at the pattern.

routes.define :user, '/users/{id}'
routes.define :users, '/users{?name,country}'

I propose having a query param to build a query string, just like it happens with fragment, so the second example would look like

routes.uri_for(:users, query: {name: 'alice', country: 'argentina'})

This helps getting rid of the "{?name,country}" syntax when defining the url, while also permitting it in any definition.

Also, what do you think of a way of returning a URI object? Maybe via a parameter? I'm not sold on this, but just wanted to throw it out there.

My 2 cents :)

@moonglum
Copy link
Owner Author

Hey @tonchis – glad to hear from you ❤️ Haven't read you in a while 😉 Thanks for the feedback! Keep it coming 😄

Query Parameters

That's a very good point, yes. This would also create a parity to the other "special parameter" fragment. But shouldn't the path parameters than also be an object under the name path?

routes.uri_for(:user, path: { id: 13 })

On the other hand this would be quite unwieldy.

The reason why I did it the way I did, is that I want to map resources to URIs. So let's imagine I have a user object. Let's say it is an Ohm::Model, then I can ask for the Hash representation via user.attributes. If the user has a field id, I could for example use our URI template from the example and do this:

# routes.define :user, '/users/{id}'
routes.uri_for(:user, user.attributes) #=> '/users/13'

If I however change my URI scheme and provide the ID as a query parameter, I would not need to change the uri_for call:

# routes.define :user, '/users/{?id}'
routes.uri_for(:user, user.attributes) #=> '/users?id=13'

And I think this is really, really important. Why? Because it does not matter to the model how the URIs are structured. This is not part of the models job. It is probably also not the job of the caller of the method: With query and path parameters being on the same 'level', the details of the URI are decoupled from the caller of the method.

URI Object

I experimented with URI objects along the way. As we are talking about HTTP, it would make sense to use URI::HTTP. But that's one weeeeeird thing... We are mostly talking about relative links, while we of course allow absolute links. So we might have a link like this: /users. We can create this link with the URI class like that:

URI::HTTP.build({:path => '/users'})

Now let me to_s that: "http:/users". The what? 😕

@tonchis
Copy link

tonchis commented Dec 22, 2015

Query parameters

That's a very good point, yes. This would also create a parity to the other "special parameter" fragment. But shouldn't the path parameters than also be an object under the name path?

I think in this case the named parameter takes precedence, so it feels ok to call

routes.uri_for(:user, id: 13)

when dealing with

routes.define(:user, "/users/{:id}")

The reason why I did it the way I did, is that I want to map resources to URIs. So let's imagine I have a user object. Let's say it is an Ohm::Model, then I can ask for the Hash representation via user.attributes. If the user has a field id, I could for example use our URI template from the example and do this:

Now I understand the motivation and makes more sense to me.

And I think this is really, really important. Why? Because it does not matter to the model how the URIs are structured. This is not part of the models job.

Agree 100%.

It is probably also not the job of the caller of the method: With query and path parameters being on the same 'level', the details of the URI are decoupled from the caller of the method.

Not so sure here. If the caller wants urls I think it's ok to assume it has some knowledge of their parts. And if the use case is recurrent, they can create a helper method to pass all the values in one hash.

URI Object

Ugh, that URI::HTTP#to_s is terrible, yes. How about using just URI?

URI("/users").to_s # => "/users"

And just leave the protocol up to the parser in URI.(). Of course, if the user wants a URI object, they can just build it themselves by doing

URI(router.uri_for(:users))

So it comes down to what makes better sense in the context of Shigeru ¯_(ツ)_/¯

@soveran
Copy link

soveran commented Dec 22, 2015

URI Templates are very powerful. But would something less powerful do the trick as well?

If you can get a 90% solution for 10% the code, it's a deal you should never reject :-)

@elcuervo
Copy link


So much food for thought ^_^

@moonglum
Copy link
Owner Author

Query Parameter @tonchis

  • Precedence: Yes, that's true 😄
  • Motivation: Ok 😄 That means: I have to add information to the README!
  • "If the caller wants urls I think it's ok to assume it has some knowledge of their parts." That's a good point, I think it's time to think about the usage of this lib – see below

URI Object @tonchis

That's a good point, yes! So URI, not URI:HTTP ☑️ Again, see below.

90% solution for 10% of the code @soveran

Yep, agreed! But I think that requires determining how much of the solution it would cover – and that requires determining the usage of this lib – see below 😸

Popcorn Time @elcuervo

Hehe ❤️

Usage for this lib

I think the use case for this lib is to be combined with a web framework like Tynn, Cuba or Sinatra. It asks you to define the routes next to the routes you defined yourself. For example:

require "tynn"
require "shigeru/tynn"

Tynn.plugin Shigeru::Tynn # an imaginary Tynn plugin
Tynn.define do
  root do
    get { ... }
    post { ... }
  end
  on 'users' do
      get { ... }
      on :user_id do
        get { ... }
      end
  end
end

# This uses the Tynn plugin now
Tynn.define_route :root, '/'
Tynn.define_route :users, '/users'
Tynn.define_route :user, '/users/{id}'

Now those routes are mainly used in views, for example:

<nav>
  <ul>
    <li><a href="{{ app.uri_for :root }}">Home</a></li>
    <li><a href="{{ app.uri_for :users }}">Users</a></li>
    <li><a href="{{ app.uri_for :user, current_user.attributes }}">Your Profile</a></li>
  </ul>
</nav>

What do you think? Would that be something useful for you? Do you see other use cases?

@snusnu
Copy link

snusnu commented Dec 23, 2015

hey!

just the other day i was writing a tiny layer on top of the webmachine router DSL that allows me to a) give names to routes, b) use those names (instead of webmachine resources) to find particular routes and c) generate URLs for them. what i currently have (i'll soon need more, i.e. support for query params) is very basic and can only handle path param interpolation of hashes. then i stumbled over this project and naturally i'm interested :)

since you're asking for use cases, here's what i'm thinking would be nice: a library focusing on the definition of a set of named routes along with a proper object model (or AST) that exposes these defined routes. such api could then be used to generate route definitions for different webframeworks programmatically, and of course for (webframework independent) URL generation.

imo there should be just one place/concept in a typical app that knows about routes, and knows about them "deeply". if the information is rich enough (and imo there isn't that much to it for your typical webapp), it should be easy enough to generate code to adapt to different router DSL flavors and of course to generate URLs.

i realize that this might be completely out of scope for what you had in mind for this lib, but i thought i'd share my thoughts! i'd surely be happy to drop my halfbaked url generation code for something i could use to generate my webmachine routes from, plus related URLs.

@tonchis
Copy link

tonchis commented Dec 23, 2015

@moonglum Yes, thinking about the usage/use cases is the best approach to solve what the lib needs and what it doesn't.

I see your example as the most typical use case, filling up hrefs in templates, which get interpolated, so there's no need to have the lib return URI objects when a string is easier to debug. Specially since URI(string) is enough to build it.

Now, about the interface, if I were the one writing this (and I'm not), I'd provide named parameters for the routes.define populated by keyword arguments in routes.uri_for, plus a query and a fragment keywords on that same method.

My reason behind this is the fact that while it is true that current_user.attributes is a flat representation of the object which could be distributed evenly between path and query, the querystring is usually the place to append tracking params (such as utm_source and all that jazz), either third party or known to your app, and it feels strange to do something like:

routes.uri_for(:user, current_user.attributes.merge({utm_source: "foo", id_of_something_else: "bar"}))

And then handling that one big hash within the routes.define template. While this is a very web thing, I mention it because the example is an html template.

The only advantage I see on having routes.define know about the form of the query is to validate it when it gets built. But I think that's what unit tests are for.

Having said all this, a lib to handle the url generation within the context of the business logic of an app is very much in need, and we can hone it as we see how it's used in the real world.

@snusnu welcome! great to have you in the thread. I'd really like to see some of the code you mentioned in your use case. As I said above, real world use cases will help finding what's important.

@moonglum
Copy link
Owner Author

Sorry, was busy for the last few days 😉 Here I am, back again! Hey @snusnu, glad to get your take on it 😄 Also thanks to @railsbros-dirk and @nerdbabe for discussing it with me in person ❤️

a library focusing on the definition of a set of named routes along with a proper object model (or AST) that exposes these defined routes.

I'm wondering how you would wire up the routes to the code that should run accordingly. Or would you do that as a part of your AST transformation? I started hacking on a mashup of @soveran's syro with ideas from here. Not sure where it will lead me 😄 Definitely interested in your code 😄

imo there should be just one place/concept in a typical app that knows about routes, and knows about them "deeply".

I'm not entirely sure. On the one hand the advantage of having it one central place is clear: No duplication, all knowledge is in one place. On the other hand I'm not entirely sure if we are not talking about two responsibilities:

  • Given an incoming request, which "action" does it match to? In other words, match an URI to a resource.
  • Given a resource or information about a resource, which is the URI for it? In other words, match a resource to an URI.

But there is of course a connection between those two, as one is the inverse function of the other...

@tonchis: Ok, so no URI 😄

About the interface of uri_for: Interesting! When I think about query parameters, I usually don't think about utm_source, I think about things like parameters for a search in a GET request: /users?name=tonchis. And I think that's a pretty fundamental difference in thinking about the problem. I think about it as a mapping from resources to URIs. When we talk about that, there's no thought about utm_source, but it is of course a valid concern.

I think that by using an URI template, we can whitelist the parameters – and by that allow the user to really provide an entire object (or its representation as a hash) to the method to get an URI for that object. And in this way of thinking, it makes a lot of sense to not let the caller think about different kinds of parameters. But I'm not sure how to deal with "additional information" like utm_source in a solution like that.

@soveran
Copy link

soveran commented Dec 29, 2015

imo there should be just one place/concept in a typical app that knows about routes, and knows about them "deeply".

I'm not entirely sure. On the one hand the advantage of having it one central place is clear: No duplication, all knowledge is in one place. On the other hand I'm not entirely sure if we are not talking about two responsibilities:

Given an incoming request, which "action" does it match to? In other words, match an URI to a resource.
Given a resource or information about a resource, which is the URI for it? In other words, match a resource to an URI.
But there is of course a connection between those two, as one is the inverse function of the other...

I think this is similar to defining what message to send and how to implement it. A system can define the messages needed for it to work, and then it can decide where and how to implement the methods for handling those messages. But sending a message is not tied to a method definition. In this example, sending the message is something done by the browsers, and this library defines which messages the browser should send. Then the actual routes are implemented somewhere else, with any means necessary to accomplish the desired goal. It doesn't matter which routing library you use.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants