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. Deprecating ActiveResource An Alternative Approach for Internal Rails Services Gabe

    da Silveira! Tech Lead @ MUBI
  2. 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
  3. The Good

  4. 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 = 'ren@gmail.com'! 12 @person.save!
  5. 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: 'stimpy@gmail.com')! 12 # => PUT http://example.com/people/1.json! 13 ! 14 @person.destroy! 15 # => DELETE http://example.com/people/1.json!
  6. Terse 1 class Person < ActiveResource::Base! 2 self.site = "http://example.com"!

    3 end!
  7. Pitfalls

  8. 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
  9. 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
  10. 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
  11. 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
  12. 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.
  13. 0 40 80 120 160 2006 2007 2008 2009 2010

    2011 2012 2013 2014 Commit Activity
  14. Our Solution

  15. 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:
  16. 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
  17. Private Gem Strategy Main Application Billing Service JSON Before:

  18. Private Gem Strategy Main Application Billing Service Private Gem After:

  19. 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!
  20. 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!
  21. 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!
  22. 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!
  23. 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!
  24. 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
  25. Thank You Gabe da Silveira! gabe@mubi.com @dasil003