Defining a "canonical" href path for an API Resource?

22 views
Skip to first unread message

Josep Blanquer

unread,
Feb 9, 2015, 8:29:44 PM2/9/15
to praxis-de...@googlegroups.com, praxis-...@googlegroups.com
All,

 So here's a question about a possible feature request we can add to Praxis, and I'd like to see what people think.

 It is often the case that you need to manage/generate API URLs within the API code. I think that these fall usually into these two categories:
  1.  generating hrefs. For example, to return as an HTTP redirect, or to embed "links" within a resource media_type or response.
  2.  parsing received href parameters. For example, receiving hrefs within a create or update request, from which you need to parse their ids and perhaps resource types...
So, right now, Praxis allows some ways to do most of those things, but they are very low level. (And not well documented, actually). For example, if you have an action defined in your Blogs resource definition like this:

class Blogs 
  include Praxis::ResourceDefinition        
  media_type MediaTypes::Blog
 
  action :show do
    routing { get '/:id' }
    params do
      attribute :id
    end
  end
end

you can get a hold of the action path with:
resource_path = Blogs.actions[:show].primary_route.path

which then you can use to generate hrefs to it with:
resource_path.expand( id: 2)  => /blogs/2

and you can parse parameters out of an incoming href to that action by:
resource_path.match("/blogs/12345")[:id]   => "12345"

...but I think that we can do better, much better to make those processes simpler. So, to really build more help around that I propose that we introduce the concept of a "canonical resource URL". That will allow Praxis to know what URL to use to either create or parse one of a given resource type (i.e. a given ResourceDefinition). Once Praxis knows that, we can easily build wrappers to do what we want in a much cleaner way.

Here's an initial proposal:
  1. Add a "canonical_href" DSL to a ResourceDefinition class, so it can point to which of the currently defined action routes should be considered the main URL that uniquely identifies the resource type it represents. This will be typically the "show one member" action, however the application decides naming it.
  2. Add helper functions in the ResourceDefinition to generate and parse canonical hrefs to the resource (perhaps expose those from the controller too)
Here's how it could look like:

class Blogs 
  include Praxis::ResourceDefinition
        
  media_type MediaTypes::Blog
  canonical_href :show                 <<<<<<<<<<<<<<<<< NEW DSL
  action :show do
    routing { get '/:id' }
    params do
      attribute :id
    end
  end
end


And then, in the controller we could add helpers to the ResourceDefinitions to do things like:

Blogs.canonical_href(id: 123) => /blogs/123
Blogs.parse_href("/blogs/123")  => {id: 123}  #which I believe we can even coerce to the right type

Note that routes can perfectly have many more than a simple :id attribute.

We can also expose these helpers as instance or class methods in the controller (as opposed to class methods in the ResourceDefinitions) if we thought it was cleaner. That is easy since controllers have a pointer to their corresponding ResourceDefinitions.

I'm not really liking the names...but I think you get the gist.

Also note that if a given action has more than one route defined, we'd always use the 'primary' one. That seems to be a reasonable thing as it is easy to move the canonical route to the first one in the routing block, plus one can easily name the routes appropriately if need be)

Thoughts? Does this sound like something you'd like to use? do you have suggestions as to how this can be exposed better?...

Josep M.

Sean McGivern

unread,
Feb 16, 2015, 4:36:23 AM2/16/15
to Josep Blanquer, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
As I brought this up initially, I'd better +1 it :-) Coercing the named matches to the right types sounds especially good, although all of the cases we need it for at the moment are just IDs.

One thing I'm not sure about is if canonical_href should be implied if not given. I can't think of a situation where having a mistaken canonical_href value would be harmful (just don't use it), but I wonder if it might be too surprising and 'magical' if it was always available.

--
You received this message because you are subscribed to the Google Groups "praxis-support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to praxis-suppor...@googlegroups.com.
To post to this group, send email to praxis-...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/praxis-support/CAA_K6YsijZ3YP9BJo7Exikya2VSFt4U0%2B8c4zPudVT2JyJCZ-g%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Josep Blanquer

unread,
Feb 18, 2015, 1:00:27 PM2/18/15
to Sean McGivern, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
Ok. I had some small amount of time last night to toy with this and I have the backing pieces together to make this happen (i.e. Basically I've verified that all this can be done without issue, including coercing the href parameters to the right types).

So now, to me, the main question is how do we want to name and use these things. I'm not necessarily happy with the DSL/naming so I would appreciate feedback from everybody about it.

Here is how I have it now implemented in my quick prototype
  • You can define which action should be used for the canonical href by:
    1. Either at the top-level of a resource definition by denoting which action with:
      1. canonical_path_action :show   << action :show (primary route) is the canonical href for this resource definition
    2. Or simply doing it within the action definition block itself with:
      1. canonical_path
    3. If none has been specified it will default to the :show action if it exists.
  • Then you can either parse or generate hrefs by:
    1. reaching directly into the resource definition static functions: MyResourceDefinition.to_href(*) or MyResourceDefinition.from_href(*) 
    2. or doing it from the controller `definition` method: self.definition.to_href(*) or self.definition.from_href(*)  (when you're trying to parse or generate hrefs for the same resource type of the controller)
Here's an example of that from the spec app in Praxis:

The Instances ResourceDefinition would have:

module ApiResources
  class Instances
    include Praxis::ResourceDefinition

    media_type Instance
    version '1.0'

    canonical_path_action :show
...
... # Or within the action:
    action :show do
      canonical_path
    end
end

And within a controller action you can:

def some_action(cloud_id:)

      # Construct the location by generating the canonical URL of this resource.
      # i.e. /clouds/1/instances/787
      self.response.headers[ 'Location'] = definition.to_href(cloud_id: cloud_id, id: instance.id)

      # You can also parse an incoming URL into the coerced params hash like:
      self.definition.from_href("/clouds/1/instances/787")  # Which would return => {:cloud_id=>1, :id=>787} 
      # Which is equivalent to:
      ApiResources::Instances.from_href("/clouds/1/instances/787")  # Which would return => {:cloud_id=>1, :id=>787}
      # where typically the href string might come from a received param or payload attribute
      ....
    end

Note that the result of the .to_href coerces the parameters accordingly to what the action defined. In this case those parameters are
defined as Integers, therefore the resulting hashes will have integers. While String/Integers might be too simple, this can come really handy when
they are defined as more precise types like UUIDs or custom-specific types... 

So...what are you guy's thoughts about where in the DSLs you'd like to define these things, how you'd like to name them, and how you'd like to name and use the href parsing/generator helpers?

 Cheers,

Josep M.

Sean McGivern

unread,
Feb 20, 2015, 5:36:29 AM2/20/15
to Josep Blanquer, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
The DSLs look fine, and the only name I personally have a problem with is `from_href`. To me, when I read `self.definition.from_href`, that doesn't scream 'you will get a hash'. Maybe `params_from_href` or something to indicate what it is that you're getting out of the href? (`to_href` is fine, obviously.)

I'm also not clear on what happens if you mistakenly define more than one `canonical_path`. Is an exception raised, or does the last one win?

Josep Blanquer

unread,
Feb 21, 2015, 11:49:33 PM2/21/15
to Sean McGivern, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
I see, how about changing `self.definition.from_href` to `self.definition.parse_href` then?

in terms of re-defining it, what I had prototyped was to raise an exception:
 
raise "Action '#{@canonical_action_name}' has already been selected as the canonical path for #{self.name}" if @canonical_action_name

Cheers,
Josep M.

Sean McGivern

unread,
Feb 23, 2015, 4:59:23 AM2/23/15
to Josep Blanquer, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
Sounds good, +1 to both.

Josep Blanquer

unread,
Feb 26, 2015, 1:00:14 AM2/26/15
to Sean McGivern, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
one more thing, after a few comments from the PR review.

What do you guys think about having only 1 way to define the canonical path...instead of having the option at the resource level or at the action level?

Can you take a look at this comment and say what you think?

I tend to agree...simplifying the options is good?

Josep M.

Sean McGivern

unread,
Feb 26, 2015, 12:17:52 PM2/26/15
to Josep Blanquer, praxis-de...@googlegroups.com, praxis-...@googlegroups.com
Yep, both comments there make sense to me.
Reply all
Reply to author
Forward
0 new messages