Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deprecating ActiveResource

Deprecating ActiveResource

MUBI has a long and bumpy history with ActiveResource, which eventually led to enough pain to migrate away. This talk explores the why and the how of this ongoing transition.

Gabe da Silveira

May 12, 2014
Tweet

Other Decks in Programming

Transcript

  1. What is ActiveResource? • Library for consuming RESTful APIs •

    Designed to look and feel like ActiveRecord • Leans heavily on convention and defaults • Formerly part of Rails core; pulled from 4.0
  2. Familiar interface 1 # Find a single person! 2 Person.find(1)!

    3 ! 4 # Find all persons! 5 Person.all! 6 ! 7 # Create a person! 8 Person.create(name: 'Ren Höek')! 9 ! 10 # Edit a person! 11 @person.email = '[email protected]'! 12 @person.save!
  3. Rails' REST conventions 1 @person = Person.find(1)! 2 # =>

    GET http://example.com/people/1.json! 3 ! 4 Person.all! 5 # => GET http://example.com/people.json! 6 ! 7 Person.create(name: 'Stimpson J. Cat')! 8 # => POST http://example.com/people.json! 9 ! 10 @person.update_attributes(! 11 email: '[email protected]')! 12 # => PUT http://example.com/people/1.json! 13 ! 14 @person.destroy! 15 # => DELETE http://example.com/people/1.json!
  4. Grain of Salt • We started with a very early

    version of ARes • We have questionable monkey-patches as well • We used XML the whole way
  5. 1 # Given the following JSON structure:! 2 { "id"

    => 1,! 3 "name" => "Mr. Horse",! 4 "address" => {! 5 "street" => "Gritty Kitty Studios",! 6 "state" => "CA" } }! 7 ! 8 # Returned by a request to:! 9 @mr = Person.find(1)! 10 ! 11 # If you have defined an Address class then you get:! 12 @mr.address # => #<Address::xxxxx>! 13 @mr.address.class # => ::Address! 14 ! 15 # However if you have no Address class defined Ares creates one:! 16 @mr.address # => #<Person::Address::xxxxx>! 17 @mr.address.class.superclass # => ActiveResource::Base! Unpredictable deserialization
  6. Tends to break between Rails versions • JSON/XML serialization changes

    percolate upwards • ActiveResource is usually patched out of necessity • Solid test coverage is difficult to achieve
  7. Incomplete functionality • ARes suggests complete ActiveRecord-like functionality, but supports

    only a fraction • Even supported features tend to differ from AR • Semantics have been decided in ad-hoc fashion by individual contributors
  8. Why isn't ActiveResource better? • A RESTful API is not

    a solid, well-specified substrate to construct a DSL against. • ARes mimics ActiveRecord instead of engaging with its problem domain from first principles. • ActiveResource has a small user base and no visionary providing leadership.
  9. 0 40 80 120 160 2006 2007 2008 2009 2010

    2011 2012 2013 2014 Commit Activity
  10. What are we building? Billing Service Main Application Responsibilities:! •

    Credit Card Storage! • Subscription Creation! • Monthly Billing Standard Rails App:! • Public web interface! • Services platform APIs (iOS, Android, Sony, Samsung, etc) MUBI is a video streaming service:
  11. Goals of an ActiveResource replacement • Keep same "active" nature

    of remote models • Guarantee correct serialization and typecasting • Normalize to rich Ruby objects early • Build for the future, but stay cognizant of YAGNI
  12. A Model 1 module BillingService! 2 class Subscription < Model!

    3 define_attributes(! 4 id: :int,! 5 user_id: :int,! 6 country: :country,! 7 status: :string,! 8 expires_at: :datetime,! 9 price: :money,! 10 credit_card: :credit_card,! 11 )! 12 end! 13 end!
  13. Typecasting 1 Subscription.new(! 2 expires_at: "2014-05-12T01:02:03Z")! 3 Subscription.new(! 4 expires_at:

    Time.utc(2014,5,12,1,2,3))! 5 Subscription.new(! 6 'expires_at(1i)' => '2014',! 7 'expires_at(2i)' => '5',! 8 'expires_at(3i)' => '12'! 9 'expires_at(4i)' => '1'! 10 'expires_at(5i)' => '2'! 11 'expires_at(6i)' => '3')! 12 ! 13 @subscription.expires_at #=> 2014-05-12 01:02:03 UTC!
  14. A Client 1 module BillingService! 2 class SubscriptionClient < Client!

    3 def all! 4 execute_get("/subscriptions")! 5 end! 6 ! 7 def find_by_user(user_id)! 8 execute_get("/users/#{user_id}/subscription")! 9 end! 10 ! 11 def save(subscription)! 12 execute_save('/subscriptions', subscription)! 13 end! 14 ! 15 def cancel(subscription, params)! 16 params = hard_filter_params(params, :reason_for_cancellation)! 17 ! 18 execute_put("/users/#{subscription.user_id}/subscription/cancel", params)! 19 end! 20 end! 21 end!
  15. Models declare client interface 1 module BillingService! 2 class Subscription

    < Model! 3 define_attributes( ... )! 4 ! 5 expose_client_class_methods :all! 6 expose_client_class_methods :find_by_user, singular: true! 7 expose_client_instance_methods :save, :cancel! 8 end! 9 end!
  16. Error Handling 1 module MUBI! 2 ERROR_CODES = {! 3

    1 => :missing_params,! 4 2 => :invalid_params,! 5 # ...! 6 }! 7 end! 8 ! 9 module MUBI! 10 class APIError < StandardError! 11 def self.deserialize(hash); end! 12 ! 13 def initialize(symbol, details={}); end! 14 ! 15 def as_json; end! 16 end! 17 end!
  17. Lessons Learned So Far • API conventions should be influenced

    by a standard such as JSON API, not ActiveResource • ActiveResource is a leaky abstraction • APIs are good places to be explicit rather than relying on Rails-style magic • ActiveResource's ruby API is nice enough when it works, but probably impossible to design for the general case