0.0
No commit activity in last 3 years
No release in over 3 years
See readme
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 11.0
>= 3.2
~> 3.0

Runtime

 Project Readme

RemoteResource

Build Status Code Climate Test Coverage

RemoteResource allows you to easily create ActiveRecord style domain objects that represent a foreign API. These remote resources can be mixed into or associated with other ActiveRecord models in the same way you work with all your other models. Using these conventions yields some major performance gains through caching and fast and simple development through familiarity.

Why RemoteResource

  • Familiar - The DSL used to wrap foreign APIs is simple and intuitive. Using the remote resource will be familiar to anyone who has worked with ActiveRecord models.

  • Reusable - Write your API interface once. Associate it with an ActiveRecord object, embed it into a value object, or instantiate it for use in a service.

  • Performant - API responses are transparently cached. Subsequent calls move at the speed of redis. Etag based cache expiring, which you may override. Makes detailed list pages possible.

  • Resiliant - Easy to configure error handling, just like ActionContoller. Use cached values to rescue momentary network failures.

Getting started

Create a remote_resource, such as:

# in app/remote_resources/github_user.rb
class GithubUser < RemoteResource::Base
  client { Octokit::Client.new }
  resource { |client, scope| client.user(scope[:github_login]) }

  attribute :id
  attribute :avatar_url

  ...
end

Associate it with your ActiveRecord User model:

# in app/models/user.rb
class User < ActiveRecord::Base
  has_remote :github_user, scope: :github_login

  ...
end

And you now have an associated remote resource, that you can use just like you local models.

user = User.find(1)

user.github_user.id
user.github_user.avatar_url

Behind the scene, has_remote evaluated the scope on user 1 and issued a get request to the GitHub API for the GithubUser with (local) User #1's github_login. The response is cached and future github_user calls will be fast!

Installation

Add this line to your application's Gemfile. Please note the hyphen

gem 'remote-resource'

And then execute:

$ bundle

Or install it yourself as:

$ gem install remote-resource

Defining an RemoteResource

By convention, resource classes are located under app/remote_resources. This folder is automatically added to your Rails eager loaded paths.

# In `app/remote_resources/github_user.rb
class GithubUser < RemoteResource::Base
  client { Octokit::Client.new }

  resource { |client, scope| client.user(scope[:github_login]) }

  rescue_from Octokit::Unauthorized

  attribute :id
  attribute :avatar_url
  attribute :url
end

The are 4 class methods that are available to help define an (API) remote resource. They are:

  • client: You return an instance of the web client that the API uses in a block. That block yields the scope. (More on scope later.)

  • resource: Supply a block to the resource method that returns a a remote resource. For example, the 'show user' response (GET /user/:github_login) that returns information about a specific user. The return value should respond to to_hash in order to be used with attribute. Optionally takes a symbol argument, specifying a name so that it may be looked up later.

  • attribute: A single piece of data from the resource (or web) response. This will be mapped to a method later. Optionally takes a second symbol argument referring to a non-default resource (with an argument).

  • rescue_from: Works in the same way that ActionController's rescue_from works. It takes one or many Error class(es), and either a block of a :with option that refers to an instance method on this class. The block or instance method receive the error and an additional context hash as arguments.

Remote resource allows you to define any instance method you like on it, which may be used by being instantiated itself or from an associated model.

The following instance methods are available within a RemoteResource::Base class.

  • client - returns the evaluated client block.

  • resource(resource_name) - returns the evaluated resource block for the provided name. An optional argument returns the evaluated resource block with that name.

  • with_error_handling - code executed within a block to this function will have this classes' error handling (from the rescue_from methods) enabled. It takes an optional options Hash which will be sent to error handling block or method which to allow for context specific behavior.

  • the attributes - all of the attributes named in the class method are available as methods in the instance. Attribute methods always return strings.

# In `app/remote_resources/github_user.rb
class GithubUser < RemoteResource::Base
  client { Octokit::Client.new }
  resource { |client, scope| client.user(scope[:github_login]) }
  attribute :name
  rescue_from Octokit::Unauthorized, with: :swallow_validate

  def markdown_summary
    with_error_handling action: :get_markdown do
      client.markdown "# A big hello to #{name}!!!"
    end
  end

  private

  def handle_fetch(exception, context)
    raise exception unless context[:action] == :validate
  end
end

In the above examples, the markdown_summary method returns a string containing a small HTML fragment. The method body uses with evaluated client block which is an Octokit client in this case. Before sending a string to be markdown-ified, the name attribute is looked up. This is wrapped inside of a with_error_handling block to catch any potential errors.

The private handle_fetch method above is a configured error handler, specified on the above rescue_from call. In this case, it re-raises all Unauthorized errors expect for when the action is :validate.

The above markdown_summary method may be used from an associated User as follows. The handle_fetch method may not be used because it is private.

user = User.find(1)
user.github_user.markdown_summary

Instantiating Remote Resources directly

The above GithubUser example may also be instantiated on it's own. The initializer takes the scope argument as an options Hash. In this case, because in our resource block, we use scope[:github_login], we send a :github_login option into the constructor. For example:

github_user = GithubUser.new(github_login: 'mkcode')

Now that we have an instance, we may call any of our custom defined methods on it.

github_user.markdown_summary
#=> "<h1>A big hello to Chris Ewald!!!</h1>"

We also may call any of our defined attributes.

github_user.name
#=> "Chris Ewald"

The scope

The scope option evaluates the keys of the Hash on the object specifying it. There are a few different ways to define the scope, but it is always sent into the client and resource blocks as a symboled key / value Hash. Consider the following lines evaluated inside a User model: class User < ActiveRecord::Base

  • has_remote :github_user, scope: { id: :github_id } - The scope is a Hash. The :github_id method will be called on the User and sent as the value of the :id key into the RemoteResource. Ex: scope = { id: 234562 }

  • has_remote :github_user, scope: :github_id - The scope is a single Symbol. Like above, the :github_id method will be called on User, except the value will be sent under a :github_id key. Ex: scope = { github_id: 234562 } This is just a shorthand for when the method on the calling object and the scope key are the same.

  • has_remote :github_user, scope: [:github_id, :access_token] - The scope is an Array. Both the :github_id and :access_token methods will be called on User and sent in under the same keys. Ex: scope = { github_id: 234562, access_token: "af98f73qfh37ghf374h34rt9" }

Once evaluated, scopes will remain frozen for the lifetime of a RemoteResource instance. They are also used as piece of the cache_key.

Is or has

Two methods are available for your model classes. has_remote and embeds_remote. They take all the same options and do mostly the same thing; create a method on the calling object, which returns that records associated RemoteResource instance. embeds_remote will go one step further and define all of the attribute getter methods on the calling class as well. This can be used to create flat domain objects, or possibly value_objects, which are backed by values from a remote API. This is largely related to Inhertance vs Composition in programming theory which you are welcome to look up on your own time. RemoteResource supports both styles; 'Is' through embeds_remote and 'has' through has_remote. If unsure, it is best to prefer composition and use has_remote over embeds_remote to create a clear distinction between your local and remote domain.

Extending other domain objects

If you do not use ActiveRecord in your app, you may still use remote-resource by simply extending the Bridge module onto whatever class you use. The has_remote and embed_remote methods will then be available. For example:

class MyPoro
  extend RemoteResource::Bridge
  has_remote :github_user
end

Configuration

In a initializer, like config/initializers/remote_resource.rb, you may override the following options:

# Setup global storages. For now there are Redis and Memory stores available.
# Default is Memory store.

# Storage::Redis takes an instance of redis client and the following options:
#
#   expires_in - Time in seconds for to keys ttl. (Default is 1 day)
#   serializer - Instance of the serializer to load and dump the response.
#                (Default is MarshalSerializer.new)
#
RemoteResource.storages = [
  RemoteResource::Storage::Redis.new(Redis.new(url:nil), expires_in: 7.days)
]

# Specify the logger RemoteResource should use:

RemoteResource.logger = Logger.new(STDOUT)

# Setup a lookup method. Only default for now, but the `validate` option
# may be changed to true or false. True will always revalidate. False will never
# revalidate. :cache_control respects the Cache-Control header.

RemoteResource.lookup_method = RemoteResource::Lookup::Default.new(validate: true)

Notifications

There are 3 ActiveSupport notifications that you may subscribe to, to do in depth profiling of this gem:

  • find.remote_resource
  • storage_lookup.remote_resource
  • http_get.remote_resource
ActiveSupport::Notifications.subscribe('http_get.remote_resource') do |name, _start, _fin, _id, _payload|
  puts "HTTP_GET #{name}"
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/remote_resource. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.