Skip to content

Instantly share code, notes, and snippets.

@elomar
Created June 7, 2010 19:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elomar/429099 to your computer and use it in GitHub Desktop.
Save elomar/429099 to your computer and use it in GitHub Desktop.
Ideias on a nicer ActiveResource API
= ActiveResource
== Guidelines
ARes is a platform to build clients to restful-like web services. It maps resources to objects that complies with the ActiveModel API.
To enjoy the advantages of being restful (whose discussion is beyond the scope of this gist), ARes should support:
* using HTTP verbs, headers and status code as they are meant;
* using hypermedia to drive application state and navigate between resources;
* different and vendored media-types
* the dinamic creation of resource types
When consuming Rails powered services, ARes defaults should match Rails defaults and ARes should work out of the box - basic crud, validations, associations. ARes should also be able to consume non-rails servers easily.
== Example client code
person = ActiveResource::Resource.at("http://example.com/people/1")
<< ActiveResource::Resource not loaded yet
# will be loadded by getting to uri when trying to access some attribute or link
person.name
person.parent # given parent is a link
<< ActiveResource::Resource, not loaded yet
person.parent.name
<< automatically gets parent href
person.parent.as("application/vnd.parent+xml")
person.parent.update_attribute(:name, "Paul")
# issues PATCH (or PUT) request to parent uri
person.brothers << {name: "Malena"}
person.save
# issues POST to brothers url
ActiveResource::Resource.at("http://example.com/people/1").as(:xml) # uses built-in xml formatter
ActiveResource::Resource.at("http://example.com/people/1".as("application/vnd.sun.Status+json") # uses built-in json formatter
ActiveResource::Resource.at("http://example.com/people/1").as(MyMediaTypeFormatter)
ActiveResource.at("http://example.com/people/1").get / post / put / delete
ActiveResource.at("http://example.com/people/1").update_attribute / save / destroy
ActiveResource.at("http://example.com/people").first / all / last / new / create
ActiveResource.at("http://example.com").at("/people")
class Person < ActiveResource::Base
at "http://example.com"
as :xml
end
class Person < ActiveResource::Base
site "http://example.com"
media_type :xml
end
Person.all ...
@technoweenie
Copy link

I would personally like uri/content_type vs at/as. Those seem pretty easy to confuse.

Also, the #as method could use the Mime::* constants that are already defined elsewhere:

# config/initializers/mime_types.rb
Mime::Type.register "application/vnd.tender-v1+json", :ts1

Do you want this to be completely incompatible from existing ActiveResource usage? The doc mentions hypermedia, but not how it's used in the response body. There's also what looks like an association update: person.brothers << {name: "Malena"}.

I experimented with hyperlinks in the body on the Tender API. For instance, the API response for this discussion has keys like:

(yes, the queue URL sucks, sue me :)

"queue_href": "http://api.tenderapp.com/github/discussions/673039/queue?queue={queue_id}"
"category_href": "http://api.tenderapp.com/github/categories/10118"
"html_href": "http://support.github.com/discussions/feature-requests/801"

This could possibly be in a nested hash, like:

"href": { "queue": "...", "category": "...", ... }

I also like using URI templates. Getting a category gives you this:

"discussions_href": "http://api.tenderapp.com/github/categories/10118/discussions{-opt|/|state}{state}{-opt|?|page,user_email}{-join|&|page,user_email}"

That means you can do this:

http://api.tenderapp.com/github/categories/10118/discussions
http://api.tenderapp.com/github/categories/10118/discussions/open
http://api.tenderapp.com/github/categories/10118/discussions?page=2
http://api.tenderapp.com/github/categories/10118/discussions/open?page=3&user_email=...

Have you looked at Faraday? I wrote that because ActiveResource was too limited to handle this JSON miniapp that we wrote for a client. I wanted something like Faraday to power ActiveResource, so that if you have a good starting point if you have to leave it behind. It's basically RestClient with a rack-like extension API.

The GitHub API in its current form will likely never run on ARes due to the funky "resource" paths. But, I still want a nice API library that I can use for end-to-end integration testing.

@josevalim
Copy link

Hey Rick!

  1. Backwards compatibility is a goal!

  2. "The doc mentions hypermedia, but not how it's used in the response body."

I'm not sure if I understood your sentence properly, but Elomar will add ATOM support using . However, for XML and JSON I would prefer if the links are sent through HTTP Headers.

  1. person.brothers << {name: "Malena"}

The above is adding a new item to the association. I would personally like if we can GET/POST/UPDATE resources without a need to create classes, only using hashes. Do you see something wrong with the approach?

  1. The formatter used in as, afaik goes beyond Mime::XML because it should also know how to parse the content. The mime type can be Mime::XML, but the content could be represented in different ways (and we should probably share these formatters with ActionPack).

  2. About URI templates I have to study about it a bit before giving my feedback, but Restfulie guys are using OpenSearch for queries as the one you sent. I think we won't have time to implement any of these options, which is good, because it will give more time for the Ruby community to choose a preferred option.

  3. I asked Elomar to look at Faraday and it seems he enjoyed it! We should target to integrate it until the end of summer! :)

You definitely have good experience with all this, so your feedback is definitely welcome! Cheers!

@technoweenie
Copy link

  1. Ah HTTP Link headers, I can dig that. Riak does the same thing. It's funny when curl blows up because Riak Buckets have a massive Link header pointing to several thousand documents :)

  2. I wasn't sure if that was in the person body or not. Sounds good.

  3. Sure, but #as also takes strings and symbols in the examples. I'm just suggesting Mime classes could be another option. I prefer that to hardcoding "application/vnd.sun.Status+json".

  4. OpenSearch is focused on paginate links, not necessarily the URL templating. Something like http://example.com?q={searchTerms}&amp;c={example:color?} isn't clear on how to handle the case where the color option isn't given. Is there just an empty ?c param? URI Templates handle that. The syntax is nasty, but it works well. Addressable has a parser.

Though I think you're right that Rails can specify that the value is a string, and leave the rest up to the API lib.

  1. Sounds good. I'll help out where I can. Though I can't guarantee GitHub will ever work on ARes. I'm toying with the idea of sending back pagination info in collections (very similar to OpenSearch, only in JSON). Also, I hate how Rails adds the nested hash to every item. I understand why it's there (I campaigned for its addition and wrote that code...), but I want our API to be as awesome as possible. I wonder if custom mimetypes can solve this issue though?
class User < ARes::Base
  # maybe this is set by convention by singular/plural class name when you add
  # vendor :github
  #
  as "application/vnd.github.user+json"
  collected_as "application/vnd.github.users+json"
end

GET /users
HTTP/1.1 200 OK
Content-Type: application/vnd.github.users+json, charset=utf-8

{
  # my own custom opensearch-style properties
  "offset": 0,
  "total": 813,
  "per_page": 30,
  "users": [
    {"id": 1, "login": "technoweenie", "uri": "/users/1"}
  ]
}

GET /users/1
HTTP/1.1 200 OK
Content-Type: application/vnd.github.user+json, charset=utf-8

{"id": 1, "login": "technoweenie", "uri": "/users/1"}

@technoweenie
Copy link

Still thinking about link headers... I could just encode pagination info there.

Link: </users?limit=30&after=232232> railstag="next"; </users?limit=30&after232202> railstag="prev"

This is very loosely based on Riak link headers.

@josevalim
Copy link

About 1 to 5: agreed! URI templates sounds interesting. Definitely worth the investigation.

However, I didn't understand what you mean with: "Also, I hate how Rails adds the nested hash to every item." But yes, the issue could probably be solved by custom formatters/mime types.

@technoweenie
Copy link

Sorry, I'm only talking about JSON. It wraps every object in a hash so that you can access it like xml/form responses with something like params[:ticket].

$ curl http://rails.lighthouseapp.com/projects/8994/tickets.json | json_reformat
  "tickets": [
    {
      "ticket": { ... },
      "ticket": { ... }
    }

@josevalim
Copy link

:emo: this is indeed ugly. Maybe we could move this concern to the formatter and it would receive an array of hashes but would properly parse it and create params[:ticket] based on the root name.

@technoweenie
Copy link

Yes, based on a custom mime type. That rocks :) So much cleaner than param_parsers. That's exactly what I was thinking with that collected_as example above.

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