TL;DR: Crafting an API. Need different fields for different versions. Teach me, wise ones.
I'm currently trying to figure out the best way to craft a versioned API. That is to say, I wish to have a URL of /api/v1/projects.json that would show a list of projects with a bunch of fields and api/v2/projects.json to show a list of projects with separate fields.
I've been thinking about this problem for about 15 minutes which probably means it's all wrong. At the moment I've got this in my app/models/project.rb file:
def self.api_fields
{
:v1 => ["name"],
:v2 => ["name", "tickets_count"]
}
end
Then I can use this in my API controllers (api/v1/projects_controller.rb) like this:
def index
respond_with(Project.all(:select => Project.api_fields[:v1]))
end
This is great and works as I'd like it to, but there's probably a better way about it. That's your task! Share with me your mountains of API-crafting wisdom.
Bonus points if you come up with a solution that will also allow me to use methods for instances of a model's object, such as a tickets_count method on a Project method.
I'm agree with polarblau that you should have multiple controllers for different version of the API. So, I aim for the bonus point of this question.
I think to archive the ability to call #tickets_count, you have to override #as_json and #to_xml methods of the model. I think you'll have to do it like this:
api/v1/projects_controller.rb
def index
respond_with Project.all, :api_version => :v1
end
project.rb
class Project < ActiveRecord::Base
API_FIELDS = {
:v1 => { :only => [:name] },
:v2 => { :only => [:name], :methods => [:tickets_count] }
}
def as_json(options = {})
options.merge! API_FIELDS[options[:api_version]]
super
end
def to_xml(options = {}, &block)
options.merge! API_FIELDS[options[:api_version]]
super
end
end
However, if you don't mind the mess in the controller, I think specifying :only and :methods in respond_with call in the controller might be a good idea too, as you don't have to override those #as_json and #to_xml methods.
Just as a comment:
Have you had a look a these yet?
http://devoh.com/posts/2010/04/simple-api-versioning-in-rails
Best practices for API versioning?
devoh.com suggest to split the versions already at a routing level, which seems like a good idea:
map.namespace(:v1) do |v1|
v1.resources :categories
v1.resources :products
end
map.namespace(:v2) do |v2|
v2.resources :categories, :has_many => :products
end
Then you could use different controllers to return the different fields.
The problem is, as you know, that whatever you expose allows the end-client to create a direct dependency. Having said that, if you directly expose your models to the world, e.g. http://domain.com/products.json, whenever you change your Products model you have a limited number of options:
The end-client must live with it and behave much like a "schemaless database". You say that it's going to change, and voila it's done (reads clients will have to deal with it)!
You add a more enterprise-like versioning to your API. That's meas that at a more advanced level what you expose to the end-client are not your models. Instead you expose public objects, which in turn can be versioned. This is called a Data Transfer Object (http://en.wikipedia.org/wiki/Data_transfer_object)
If we wished to pursue the 2nd approach we could do the following:
class Project < ActiveRecord::Base
end
class PublicProject
def to_json(version = API_VERSION)
self.send("load_#{version}_project").to_json
end
private
def load_v1_project
project = load_v2_project
# logic that transforms a current project in a project that v1 users can understand
end
def load_v2_project
Project.find...
end
end
Hope it helps.
Mount a Sinatra app in routes at /api/v1 to handle your API calls. Makes it easier to add a new API and still be backwards compatible until you deprecate it.
Related
Is it possible for the route below to dynamically select different controllers or at least for a single controller to dynamically call another controller?
get '*path' => 'routing#show
For example:
/name-of-a-person => persons#show
/name-of-a-place => places#show
I recall reading something about Rails 5 that would enable this but I can't find it again to save my life. It's possible I imagined it.
Another options is to have a RoutingController that depending on which path is received will call different controllers.
The use case is I have URLs in the database with a type, and the controller depends on what type is the URL. I'm thinking something like this:
get '*path' do |params|
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
'persons#show'
when 'place'
'places#show'
end
end
I post my second best solution so far; still waiting to see if anyone knows how to do this efficiently within the routes.
class RoutingController < ApplicationController
def show
url = Url.find_by!(path: params[:path])
url.controller_class.dispatch('show', request, response)
end
end
Hat tip to André for the idea.
You could define one controller and inside its action make something like this:
def generic_show
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
controller = PersonController.new
controller.request = request
controller.response = response
controller.show
when 'place'
...
end
end
However, I would recommend you to move the code you want to reuse to other classes and use them in both controllers. It should be easier to understand and maintain.
I think you may be able to do it using advanced routing constraints.
From: http://guides.rubyonrails.org/routing.html#advanced-constraints
If you have a more advanced constraint, you can provide an object that responds to matches? that Rails should use. Let's say you wanted to route all users on a blacklist to the BlacklistController. You could do:
class BlacklistConstraint
def initialize
#ips = Blacklist.retrieve_ips
end
def matches?(request)
#ips.include?(request.remote_ip)
end
end
Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
I don't think the Rails guide example is particularly good, because this problem could essentially be solved in your application controllers before_action.
In this example, the constraint is used for IP filtering, but you could also implement matches? to check if it's a person. I would imagine something like
def matches?(request)
Person.where(slug: request.params[:path]).any?
end
And as such, the Rails router can decide whether or not to dispatch the request to the persons#show action.
Frustrating, I can't find an eligible solution for my problem.
In my Rails 4 app, I want to give my users the possibility to add their own custom post types to their sites. Like:
www.example.com/houses/address-1
www.example2.com/sports/baseball
Both would work, but only for the linked sites. Sports and houses would be the (RESTful) post types, taken from the db, added by users.
I have been struggling to find a elegant solution to accomplish this. I found http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4 but that feels kinda hacky and I'm not sure if reloading the routes works in production, I'm getting signals that it won't.
I'd say I have to use routes constraints http://guides.rubyonrails.org/routing.html#advanced-constraints but I don't have a clue how to approach this.
To be clear, I have no problem with the site setting stuff, the multi tenancy part of my app is fully functional (set in Middleware, so the current site is callable in the routes.rb file). My issue is with the (relative) routes, and how they could be dynamically set with db records.
Any pointers much appreciated.
I think route constraints don't work for you because your domain is a variable here. Instead, you should be examining the request object.
In your ApplicationController, you could define a method that would be called before any action, like so:
class ApplicationController < ActionController::Base
before_action :identify_site
def identify_site
#site = Site.where(:domain => request.host).first
end
end
As you scale, you could use Redis for your domains so you're not making an expensive SQL call on each request.
Then you can just add the #site as a parameter to whatever call you're making. I'm assuming you're doing some sort of "Post" thing, so I'll write some boilerplate code:
class PostController < ApplicationController
def show
#post = Post.where(:site => #site, :type => params[:type], :id => params[:id])
end
end
Just write your routes like any other regular resource.
I'm currently writing a rails app that has your regular resource like objects. However I would like to make my resources syncable. My web application uses web offline storage to cache results from the server (user's cannot modify data on server making syncing easier). When I fetch from the server, it returns a hash response like:
{
:new => [...]
:updated => [...]
:deleted => [...]
}
This is all well and good until I want to have a regular fetch method that doesn't do any sort of syncing and simply return an array of models
Now my question is I want to create a method in my routes.rb file that sets up routes so that I have a route to synced_index and index. Ideally, I'd be able to do something like this:
synced_resources :plans
And then all of the regular resource routes would be created plus a few extra ones like synced_index. Any ideas on how to best do this?
Note: I do know that you can modify resources with do...end syntax but I'd like to abstract that out into a function since I'd have to do it to a lot of models.
You can easily add more verbs to a restful route:
resources :plans do
get 'synced_index', on: :collection
end
Check the guides for more information on this.
If you have several routes that are similar to this, then sure, you can add a 'synced_resources' helper:
def synced_resources(*res)
res.each do |r|
resources(r) do
get 'synced_index', on: :collection
end
end
end
Wrap above method in a module to be included in ActionDispatch::Routing::Mapper.
I have resources for which it makes perfect sense to address them both as nested withing other resources and separately. I.e. i expect to use all urls like these:
/account/4/transfers # all transfers which belong to an account
/user/2/transfers # all transfers input by specific user
/project/1/transfers # all transfers relevant to a project
/transfers # all transfers
my concern is how do I write TransfersController actions (for example index) as it would double the logic found in parent models - is there a better way than doing something like
TransfersController
...
def index
if !params[account_id].nil?
#account = Account.find(params[account_id])
#transfers = #account.transfers
elsif !params[user_id].nil?
#user = User.find(params[user_id])
if #user.accesible_by?(current_user)
#transfers = #user.transfers
end
elsif !params[projects_id].nil?
.....
and the same holds for views - although they all will list transfers they will have very different headers, navigation etc for user, account, project, ...
I hope that you see the pattern from this example. I think there should be some non-ugly solution to this. Basically I would love to separate the logic which selects the transfers to be displayed and other things like context specific parts of view.
I've got an open question on this. In my question I outline the 2 methods I came up with. I'm using the second currently, and it's working pretty well.
Routing nested resources in Rails 3
The route I'm using is a bit different because I'm using usernames in place of the IDs, and I want them first. You would stick with something like:
namespace :projects, :path => 'projects/:project_id' do
resources :transfers #=> controllers/projects/transfers_controller.rb
end
# app/controllers/projects/transfers_controller.rb
module Projects
class TransfersController < ApplicationController
# actions that expect a :project_id param
end
end
# app/controllers/transfers_controller.rb
class TransfersController < ApplicationController
# your typical actions without any project handling
end
The reason I use the namespace instead of a call to resources is to have Rails let me use a separate controller with separate views to handle the same model, rather than pushing all the nasty conditional logic into my controller actions.
I'd like to build a real quick and dirty administrative backend for a Ruby on Rails application I have been attached to at the last minute. I've looked at activescaffold and streamlined and think they are both very attractive and they should be simple to get running, but I don't quite understand how to set up either one as a backend administration page. They seem designed to work like standard Ruby on Rails generators/scaffolds for creating visible front ends with model-view-controller-table name correspondence.
How do you create a admin_players interface when players is already in use and you want to avoid, as much as possible, affecting any of its related files?
The show, edit and index of the original resource are not usuable for the administrator.
I think namespaces is the solution to the problem you have here:
map.namespace :admin do |admin|
admin.resources :customers
end
Which will create routes admin_customers, new_admin_customers, etc.
Then inside the app/controller directory you can have an admin directory. Inside your admin directory, create an admin controller:
./script/generate rspec_controller admin/admin
class Admin::AdminController < ApplicationController
layout "admin"
before_filter :login_required
end
Then create an admin customers controller:
./script/generate rspec_controller admin/customers
And make this inhert from your ApplicationController:
class Admin::CustomersController < Admin::AdminController
This will look for views in app/views/admin/customers
and will expect a layout in app/views/layouts/admin.html.erb.
You can then use whichever plugin or code you like to actually do your administration, streamline, ActiveScaffold, whatever personally I like to use resourcecs_controller, as it saves you a lot of time if you use a REST style architecture, and forcing yourself down that route can save a lot of time elsewhere. Though if you inherited the application that's a moot point by now.
Do check out active_admin at https://github.com/gregbell/active_admin.
I have used Streamlined pretty extensively.
To get Streamline working you create your own controllers - so you can actually run it completely apart from the rest of your application, and you can even run it in a separate 'admin' folder and namespace that can be secured with .
Here is the Customers controller from a recent app:
class CustomersController < ApplicationController
layout 'streamlined'
acts_as_streamlined
Streamlined.ui_for(Customer) do
exporters :csv
new_submit_button :ajax => false
default_order_options :order => "created_at desc"
list_columns :name, :email, :mobile, :comments, :action_required_yes_no
end
end
Use https://github.com/sferik/rails_admin.