Slide 1

Slide 1 text

Deprecating ActiveResource An Alternative Approach for Internal Rails Services Gabe da Silveira! Tech Lead @ MUBI

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

The Good

Slide 4

Slide 4 text

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!

Slide 5

Slide 5 text

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!

Slide 6

Slide 6 text

Terse 1 class Person < ActiveResource::Base! 2 self.site = "http://example.com"! 3 end!

Slide 7

Slide 7 text

Pitfalls

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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 # => #! 13 @mr.address.class # => ::Address! 14 ! 15 # However if you have no Address class defined Ares creates one:! 16 @mr.address # => #! 17 @mr.address.class.superclass # => ActiveResource::Base! Unpredictable deserialization

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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.

Slide 13

Slide 13 text

0 40 80 120 160 2006 2007 2008 2009 2010 2011 2012 2013 2014 Commit Activity

Slide 14

Slide 14 text

Our Solution

Slide 15

Slide 15 text

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:

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Private Gem Strategy Main Application Billing Service JSON Before:

Slide 18

Slide 18 text

Private Gem Strategy Main Application Billing Service Private Gem After:

Slide 19

Slide 19 text

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!

Slide 20

Slide 20 text

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!

Slide 21

Slide 21 text

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!

Slide 22

Slide 22 text

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!

Slide 23

Slide 23 text

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!

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Thank You Gabe da Silveira! [email protected] @dasil003