Rails - Build adapter for several APIs - ruby-on-rails

I need help on my thoughts on building an adapter for several APIs in Rails.
Let me explain first:
I want my clients to be able to add third-party extensions to my app.
I have a model Extension and another CustomExtension.
I create the extensions myself, that appear then to the "Available extensions" section. When a user clicks on "Add extension", it goes to the "My extensions" section (which is represented by CustomExtension).
I created an extension called API Connector and i want it to work as follows:
When a user clicks on "Add extension", he picks a category (that i have defined myself) the API is part of (like "Customer Reviews"). Then, the user will enter some fields like an api_key or an api_endpoint. Then, i want to be able to connect to this api and display some other fields relevant to the api (like the name of where it comes from, example: for Customer Reviews, if a user connects the Google API for it, i want to rename the extension from API Connector to Google Customer Reviews).
In a few words, I want to be able to connect several and different APIs with the same interface and let the user do it without implementing the API in my project.
EDIT — More info:
The APIs might not have the same authentication process or the same properties. They can be very different from each other.
The technical requirements are RESTful APIs and JSON-based.

As I understand it, you want to create a way for users to connect to APIs that are unknown until runtime, based on the parameters that a user defines? If so, there's a Ruby library (now removed from Rails) that's built for allowing easy connection to REST APIs, maybe that could be of help here?
https://github.com/rails/activeresource
So, suppose I want to pull breed info from the Cat API. Here's some example code that would let me define that at runtime:
require "active_resource"
require "ostruct"
##
# This is just a data-store. It could be an ActiveRecord object or some other set
# of values that you need for the API. You'll have to establish your own criteria
# for types of API configuration you can support
#config = OpenStruct.new(
# We need to set a custom header and value
header_name: 'x-api-key',
# get your own key here: https://docs.thecatapi.com
header_value: '96120fe6-0846-41c6-9a1d-8a70e691dd47',
base_url: "https://api.thecatapi.com/v1/",
# What's the path where this resource can be found
resource_name: "breeds",
# don't add ".json" to the URLs
use_json_extension: false,
)
##
# Create an anonymous class at runtime that inherits from ActiveResource::Base
#custom_extension = Class.new(ActiveResource::Base)
##
# Config our class based on the user-provided values.
#custom_extension.class_eval do
self.include_format_in_path = #config.use_json_extension
self.site = #config.base_url
self.headers[#config.header_name] = #config.header_value
self.element_name = #config.resource_name
# Log errors here
self.logger = Logger.new("./output.log")
end
puts #custom_extension.all.to_s
With any luck, that should download a list of cat breeds for you. Which should be enough to demonstrate the concept. The docs for ActiveResource can be found here: https://rubydoc.info/gems/activeresource
Be careful that you're not importing dangerous content from a source provided by a user!
Hopefully that's what you are looking for?

Related

How to make a rails server wide object?

I am using the RedditKit gem and in order to access certain elements, I need to send a request to reddit api to create a "client" object. Below is my current logic:
## application_controller
before_action :redditkit_login
private
def redditkit_login
#client = RedditKit::Client.new ENV["reddit_username"], ENV["reddit_password"]
end
As you can see in my logic here, before EVERY REQUEST, I am subsequently making a new client object and then using that everywhere.
My question is, how do I only make one client object which can be used to serve ALL requests from anywhere?
My motive behind this is speed. For every request to server, I am making a new request to reddit and then responding to the original request. I want to have the client object readily available at all times.
You have a lot of options. A simple one would be to create a config/initializers/reddit_client.rb file and put in there:
RedditClient = RedditKit::Client.new ENV.fetch("reddit_username"), ENV("reddit_password")
(note I switched to ENV.fetch because it will error if the key is not found, which can be helpful).
You could also rename the file as app/models/reddit_client.rb. Although it's not really a model, that folder is also autoloaded so it should work as well.

Multi-tenant app with custom domains: how to populate tenant specific data in initializers and mailers?

I'm creating a multi-tenant app which uses Devise for authentication, which has an initializer file to set app specific information.
I'd also like to use the tenant's domain information in mailers.
What's the best way to store this information and populate it in the initializers and mailers?
You should not have tenant specific information including mailer information in file. Natural thing is to store information in database table. If you properly setup multi-tenant app, then database data for each tenant is completely separate from other tenants.
For example you could have table TenantInformation and put all tenant specific information like mailer connection string inside.
And depending of Rails verision you can set mailer connection option dinamicaly before sending email with :delivery_method_options check:
http://edgeguides.rubyonrails.org/action_mailer_basics.html
You can use existing solutions like https://github.com/influitive/apartment for easier work with tenants.
The best way I could come up with to properly include tenant subdomains in mailers and view links/urls is to monkey-patch url_for(which is internally used by other url helpers):
module ActionDispatch::Routing
class RouteSet
alias_method :original_url_for, :url_for
def url_for(options, route_name = nil, url_strategy = UNKNOWN)
dynamic_options = Rails.application.config.respond_to?(:dynamic_url_options) ? Rails.application.config.dynamic_url_options.call : {}
options = options.merge(default_url_options).merge(dynamic_options)
original_url_for options, route_name, url_strategy
end
end
end
Put this in lib/dynamic_url_options.rb, then require 'dynamic_url_options' at the top of your environment file. You can then do:
config.dynamic_url_options = lambda {{
:subdomain => Apartment::Tenant.current
}}
If you're prefixing your tenant names with your environment, you'll have to do some additional stripping of the staging_ prefix within this lambda.

How to set variables across models for a single request in rails?

The scenario: I need to give models access to API tokens stored in the session.
Background: I have an API-driven rails 3 application utilizing DataMapper(DM) and a DM adapter to interface with the API. Each DM model has a corresponding REST-ish API endpoint much like you get with rails scaffolding. The API requires various headers for requests, including API tokens, keys, ids etc. The headers have nothing to do with the requested data, they exist for authorization and tracking purposes only. A number of these tokens are stored in the session. I want a clean way to make these API headers available to any model during a request.
Possible solutions:
1. Passing session variables from the controller to the models
The obvious answer is passing the tokens in a hash or other object from the controller to the models. A controller action might have the following: #user = User.find(params[:id], api_headers).
The problem is needing to override any model method to accept the additional api_headers object. Not counting methods defined by Rails and DataMapper, there are hundreds of methods already defined in the application models that would need to be rewritten. So I'm ruling out a rewrite, and this also doesn't seem like a good solution since it would require overriding a ridiculous number of DM methods like the User#find example above.
2. Some metaprogramming hack
I could catch any ArgumentError's on DM's base class and check if the last argument is the api_headers object, then set the values as instance variables and invoke the requested method. This thought exercise already has me cringing at dealing with optional arguments etc. If given long enough I could probably create a functional Frankenstein that should get me fired but probably wouldn't.
3. Use a singleton (current preferred solution)
In the application controller set a before_filter to dump the session-stored API headers into a singleton ApiHeaders object. Then any model making an API request can get that singleton with the required API headers.
An additional after_filter* on the application controller would set all attributes to nil on the ApiHeaders singleton at the end of the request to prevent leaking headers between requests.
This is currently my preferred solution but I don't like that the API header values could potentially carry over into other requests if the after_filter doesn't get invoked. I don't know in which scenarios this might happen (in an application error perhaps?) which raises concerns. All I know is the values don't necessarily die with the request.
4. Custom code
Drop support of DataMapper and the custom API adapter and manually make all API calls, passing through all required API headers. Besides the fact I don't have time for this level of rewrite, why use a framework at all if you have to throw a huge chunk out to support a custom auth scheme?
Summary
What's the cleanest way to get these pesky API tokens from the session into the bowels of the application where they can be sent with each API request? I'm hoping for a better solution than those listed above.
* An alias for after_action
I set the current user and the request information on my User model using the request_store gem which is just a tiny shim over thread local storage with a bit of clean-up.
This makes the information available from any of my models via the User class. I have User.current, User.request and User.location available wherever I need it.
Your controller just has to set User.current and User.request once it has authenticated the user.
Example User model:
# models/user.rb
require 'request_store'
class User
def self.current
RequestStore.store[:current_user]
end
def self.current=(user)
RequestStore.store[:current_user] = user
end
def self.request
RequestStore.store[:current_request]
end
def self.request=(request)
# stash the request so things like IP address and GEO-IP based location is available to other models
RequestStore.store[:current_request] = request
end
def self.location
# resolve the location just once per request
RequestStore.store[:current_location] ||= self.request.try(:location)
end
end
Use Thread.current, which is passed in from request to model (note, this breaks if, inside your request, you use sub-threads). You can store the attribute you want to share in a cattr_accessor or in rails cache:
in a cattr_accessor
class YourClass
cattr_accessor :my_var_hash
...
# and in your controller
# set the var
YourClass.my_var_hash = {} if YourClass.my_var_hash.nil?
YourClass.my_var_hash[Thread.current.object_id] = {}
YourClass.my_var_hash[Thread.current.object_id][your_var] = 100
... and in your model
lvalue = YourClass.my_var_hash[Thread.current.object_id][your_var]
Note, if you use this method, you will also want to make one of the hash values a timestamp, and do some housekeeping on getting, by deleting old keys, b/c you'll eventually use up all your system memory if you don't do the housekeeping
with cache:
# in your controller
#var = Rails.cache.fetch("#{Thread.current.object_id}_var_name") do
return 100 # do your work here to create the var value and return it
end
# in your model
lvalue = Rails.cache.fetch(("#{Thread.current.object_id}_var_name")
You can then set the cache expiration to 5 minutes, or you can wildcard clear your cache at the end of your request.

Where to store editable content?

I am building a Rails app that is intended to be eventually used by non-technical people. It consists of a few pages with blocks of text and a special page with interactive canvas drawings.
I want to allow them to easily edit any piece of text contained in the application. What are the best ways to achieve that? Currently, text is written in the different views of the application, which does not allow them to edit it without having to connect via FTP or similar and search for the right file.
I am thinking of three solutions:
Store all blocks of text in the database. On each page, fetch the requires blocks and insert them before rendering. Build a page that lists all blocks in the database in editable areas with a save button.
Store all blocks of text in a json file. Create a model that can read the file and fetch the blocks required by the views. Build a page that lets you edit each block and save it in the file.
Create some kind of password-protected admin interface that fetches all file in the views directory, use regexp to find blocks of text and allow the users to edit each block and save.
From my point of view, all of my three solutions look pretty bad. It does not feel okay to do so many calls to the database? Store your entire website text in a file? Parse HTML with regexps?
What are the usual approaches used to solve this problem?
There's a great book out there: Crafting Rails 4 Applications. Here's the link to source code from the book. You will find example in templater folder. Basically, you will be able to create custom templates based on the request parameters (just like Rails does).
Update. Here's a couple of links:
Default views in Rails 3.0 with custom resolvers by José Valim (author of the book, by the way).
Implementing a Rails 3 View Resolver.
Also, here's 5 coins from me. Basically, it works like this. You need to define your own resolver and connect it to your ApplicationController (or any other controller you want):
class Resolver < ActionView::Resolver
# some code here
end
class ApplicationController < ActionController::Base
append_view_path Resolver.new
end
During the rendering process, Rails will ask your controller's resolvers to provide a template (it will go through each of them, until it finds template or until there won't be any resolvers left). In order to provide template, your resolver needs a find_templates method:
def def find_templates(name, prefix, partial, details)
# some processing here
end
So, based on this method parameters, you're going to provide some database records. But even if you have some kind of model already, Rails expects this method to return ActionView::Template instance. It can be initialized like this:
ActionView::Template.new(source, identifier, handler, details)
So, that's how your find_templates should look like:
def find_templates(name, prefix, partial, details)
template = DatabaseTemplate.find... # your custom model for DB templates
ActionView::Template.new... # initializing actual template
end
Both model and resolver in detail are presented in the book's source code (templater/3_final/app/models/sql_template.rb).
I have done that a couple times with awesome user satisfaction by using this:
http://jejacks0n.github.io/mercury/
There is also a Railscast available which gives you a good overview and step by step instructions:
http://railscasts.com/episodes/296-mercury-editor
Hope it helps. It looks good and is easy to use for end users.

Creating multiple connectors via models in their own namespace?

I have an application written in PHP using the ZendFramework, containing a library of "Connections", for example, MySQL, CSV, Twitter etc.
If a user signs up, they can connect to any data sources, say they pick a MySQL connector and a Twitter connector.
Based on the details they have entered in my UI, I would then connect to those datasources and retrieve the data they asked for.
I was hoping to set this up in Ruby on Rails but I'm not sure how. Basically what I want is:
#twitter_connection = Twitter.somefunction
#mysql_connection = Mysql.somefunction
The Twitter and MySQL models above would be in their own namespace. I called it "Connections" in my app. Can I have a namespace like that in Rails and store all the connection models in the /app/connections folder, so it would be:
/app/connections/mysql.rb
/app/connections/twitter.rb
Then, in those models, I would make the appropriate connection, whether it would be to MySQL, or an API like Twitter. Either way, the model would make the connection to the source and return whatever data I need.
Is that possible?
//Edit from comments
I have a class under '/app/connection/classname.rb'
The file contents are:
class Classname
def function
end
end
How do I access this class in a controller.
#variable = Classname.function doesn't work.

Resources