Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit b82642937ec535fb3b031c36257a4529b01c36a8 0 parents
@justinweiss justinweiss authored
3  .gitignore
@@ -0,0 +1,3 @@
+pkg/*
+*.gem
+.bundle
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in reactive_resource.gemspec
+gemspec
14 Gemfile.lock
@@ -0,0 +1,14 @@
+PATH
+ remote: .
+ specs:
+ reactive_resource (0.0.1)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ reactive_resource!
19 Rakefile
@@ -0,0 +1,19 @@
+require 'bundler'
+require 'rake/testtask'
+require 'rake/rdoctask'
+Bundler::GemHelper.install_tasks
+
+task :default => :test
+task :build => :test
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = FileList['test/**/*_test.rb']
+ t.verbose = true
+end
+
+Rake::RDocTask.new do |rd|
+ rd.main = "README.rdoc"
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
+ rd.rdoc_dir = 'doc'
+end
5 lib/reactive_resource.rb
@@ -0,0 +1,5 @@
+module ReactiveResource
+ autoload :Base, 'reactive_resource/base'
+ autoload :Association, 'reactive_resource/association'
+ autoload :Extensions, 'reactive_resource/extensions'
+end
7 lib/reactive_resource/association.rb
@@ -0,0 +1,7 @@
+module ReactiveResource
+ module Association
+ autoload :BelongsToAssociation, 'reactive_resource/association/belongs_to_association'
+ autoload :HasManyAssociation, 'reactive_resource/association/has_many_association'
+ autoload :HasOneAssociation, 'reactive_resource/association/has_one_association'
+ end
+end
78 lib/reactive_resource/association/belongs_to_association.rb
@@ -0,0 +1,78 @@
+module ReactiveResource
+ module Association
+ class BelongsToAssociation
+
+ attr_reader :klass, :attribute, :options
+
+ def associated_class
+ options[:class_name] || klass.relative_const_get(attribute.to_s.capitalize)
+ end
+
+ # A flattened list of attributes from the entire association
+ # +belongs_to+ hierarchy, including this association's attribute.
+ def associated_attributes
+ attributes = [attribute]
+ if associated_class
+ attributes += associated_class.belongs_to.map(&:attribute)
+ end
+ attributes.uniq
+ end
+
+ def resolve_relationship(object)
+ parent_params = object.prefix_options.dup
+ parent_params.delete("#{attribute}_id".intern)
+ associated_class.find(object.send("#{attribute}_id"), :params => parent_params)
+ end
+
+ # Adds methods for belongs_to associations, to make dealing with
+ # these objects a bit more straightforward. If the attribute name
+ # is +lawyer+, it will add:
+ #
+ # * lawyer: returns the actual lawyer object (after doing a web request)
+ # * lawyer_id: returns the lawyer id
+ # * lawyer_id=: sets the lawyer id
+ def add_helper_methods(klass, attribute)
+ association = self
+
+ klass.class_eval do
+ # address.lawyer_id
+ define_method("#{attribute}_id") do
+ prefix_options["#{attribute}_id".intern]
+ end
+
+ # address.lawyer_id = 3
+ define_method("#{attribute}_id=") do |value|
+ prefix_options["#{attribute}_id".intern] = value
+ end
+
+ # address.lawyer
+ define_method(attribute) do
+ # if the parent has its own belongs_to associations, we need
+ # to add those to the 'find' call. So, let's grab all of
+ # these associations, turn them into a hash of :attr_name =>
+ # attr_id, and fire off the find.
+
+ unless instance_variable_get("@#{attribute}")
+ object = association.resolve_relationship(self)
+ instance_variable_set("@#{attribute}", object)
+ end
+ instance_variable_get("@#{attribute}")
+ end
+ end
+
+ # Recurse through the parent object.
+ associated_class.belongs_to.each do |parent_attribute|
+ parent_attribute.add_helper_methods(klass, parent_attribute.attribute)
+ end
+ end
+
+ def initialize(klass, attribute, options)
+ @klass = klass
+ @attribute = attribute
+ @options = options
+
+ add_helper_methods(klass, attribute)
+ end
+ end
+ end
+end
44 lib/reactive_resource/association/has_many_association.rb
@@ -0,0 +1,44 @@
+module ReactiveResource
+ module Association
+ class HasManyAssociation
+
+ attr_reader :klass, :attribute, :options
+
+ def associated_class
+ options[:class_name] || klass.relative_const_get(attribute.to_s.singularize.capitalize)
+ end
+
+ def resolve_relationship(object)
+ id_attribute = "#{klass.name.split("::").last.downcase}_id"
+ associated_class.find(:all, :params => object.prefix_options.merge(id_attribute => object.id))
+ end
+
+ # Adds methods for belongs_to associations, to make dealing with
+ # these objects a bit more straightforward. If the attribute name
+ # is +lawyers+, it will add:
+ #
+ # * lawyers: returns the associated lawyers
+ def add_helper_methods(klass, attribute)
+ association = self
+ klass.class_eval do
+ # lawyer.addresses
+ define_method(attribute) do
+ unless instance_variable_get("@#{attribute}")
+ object = association.resolve_relationship(self)
+ instance_variable_set("@#{attribute}", object)
+ end
+ instance_variable_get("@#{attribute}")
+ end
+ end
+ end
+
+ def initialize(klass, attribute, options)
+ @klass = klass
+ @attribute = attribute
+ @options = options
+
+ add_helper_methods(klass, attribute)
+ end
+ end
+ end
+end
44 lib/reactive_resource/association/has_one_association.rb
@@ -0,0 +1,44 @@
+module ReactiveResource
+ module Association
+ class HasOneAssociation
+
+ attr_reader :klass, :attribute, :options
+
+ def associated_class
+ options[:class_name] || klass.relative_const_get(attribute.to_s.capitalize)
+ end
+
+ def resolve_relationship(object)
+ id_attribute = "#{klass.name.split("::").last.downcase}_id"
+ associated_class.find(:one, :params => object.prefix_options.merge(id_attribute => object.id))
+ end
+
+ # Adds methods for has_one associations, to make dealing with
+ # these objects a bit more straightforward. If the attribute name
+ # is +headshot+, it will add:
+ #
+ # * headshot: returns the associated headshot
+ def add_helper_methods(klass, attribute)
+ association = self
+ klass.class_eval do
+ # lawyer.headshot
+ define_method(attribute) do
+ unless instance_variable_get("@#{attribute}")
+ object = association.resolve_relationship(self)
+ instance_variable_set("@#{attribute}", object)
+ end
+ instance_variable_get("@#{attribute}")
+ end
+ end
+ end
+
+ def initialize(klass, attribute, options)
+ @klass = klass
+ @attribute = attribute
+ @options = options
+
+ add_helper_methods(klass, attribute)
+ end
+ end
+ end
+end
172 lib/reactive_resource/base.rb
@@ -0,0 +1,172 @@
+require 'active_resource'
+
+# The class that all ReactiveResourse resources should inherit
+# from. This class fixes and patches over a lot of the broken stuff in
+# Active Resource, and the differences between the client-side Rails
+# REST stuff and the server-side Rails REST stuff.
+module ReactiveResource
+ class Base < ActiveResource::Base
+ extend Extensions::RelativeConstGet
+ # Call this method to transform a resource into a 'singleton'
+ # resource. This will fix the paths Active Resource generates for
+ # singleton resources. See
+ # https://rails.lighthouseapp.com/projects/8994/tickets/4348-supporting-singleton-resources-in-activeresource
+ # for more info.
+ def self.singleton
+ @singleton = true
+ end
+
+ # +true+ if this resource is a singleton resource
+ def self.singleton?
+ @singleton
+ end
+
+ # Active Resource's find_one is broken if you don't pass a :from,
+ # which makes absolutely no sense if you're working with a singleton
+ # model and trying to generate URLs that Rails' REST support can
+ # recognize. And thus we come to this:
+ def self.find_one(options)
+ found_object = super(options)
+ if !found_object && singleton?
+ prefix_options, query_options = split_options(options[:params])
+ path = element_path(nil, prefix_options, query_options)
+ found_object = instantiate_record(connection.get(path, headers), prefix_options)
+ end
+ found_object
+ end
+
+ # Collection name is singular for singleton resources.
+ def self.collection_name
+ if singleton?
+ element_name
+ else
+ super
+ end
+ end
+
+ # This method differs from its parent by adding association_prefix
+ # into the generated url. This is needed to support belongs_to
+ # associations.
+ def self.collection_path(prefix_options = {}, query_options = nil)
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
+ "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
+ end
+
+ # Same as collection_path, except with an extra +method_name+
+ def self.custom_method_collection_url(method_name, options = {})
+ prefix_options, query_options = split_options(options)
+ "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}/#{method_name}.#{format.extension}#{query_string(query_options)}"
+ end
+
+ # This is overridden to support nested urls for belongs_to
+ # associations and removing IDs for singleton resources
+ def self.element_path(id, prefix_options = {}, query_options = nil)
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
+ element_path = "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}"
+
+ # singleton resources don't have an ID
+ if id || !singleton?
+ element_path += "/#{id}"
+ end
+ element_path += ".#{format.extension}#{query_string(query_options)}"
+ element_path
+ end
+
+ # It's kind of redundant to have the server return the foreign
+ # keys corresponding to the belongs_to associations (since they'll
+ # be in the URL anyway), so we have to try to inject them based on
+ # the current object's attributes. Otherwise, we'll lose these
+ # keys after we save an object.
+ def load(attributes)
+ self.class.belongs_to_with_parents.each do |belongs_to_param|
+ attributes["#{belongs_to_param}_id".intern] ||= prefix_options["#{belongs_to_param}_id".intern]
+ end
+ super(attributes)
+ end
+
+ # Add all of the belongs_to attributes as prefix parameters. This is
+ # necessary to support nested url generation on our polymorphic
+ # associations, because we need some way of getting the attributes
+ # at the point where we need to generate the url, and only the
+ # prefix options are available for both finds and creates.
+ def self.prefix_parameters
+ if !@prefix_parameters
+ @prefix_parameters = super
+
+ @prefix_parameters.merge(belongs_to_with_parents.map {|p| "#{p}_id".to_sym})
+ end
+ @prefix_parameters
+ end
+
+ # Necessary to support polymorphic nested resources. For example, a
+ # license with params :lawyer_id => 2 will return 'lawyers/2/' and a
+ # phone with params :address_id => 2, :lawyer_id => 3 will return
+ # 'lawyers/3/addresses/2/'.
+ def self.association_prefix(options)
+ options = options.dup
+ association_prefix = ''
+ parent_prefix = ''
+
+ if belongs_to
+ # Recurse to add the parent resource hierarchy. For Phone, for
+ # instance, this will add the '/lawyers/:id' part of the URL,
+ # which it knows about from the Address class.
+ parents.each do |parent|
+ parent_prefix = parent.association_prefix(options) if parent_prefix.blank?
+ end
+
+ belongs_to.each do |association|
+ param = association.attribute
+ if association_prefix.blank? && param_value = options.delete("#{param}_id".intern) # only take the first one
+ association_prefix = "#{param.to_s.pluralize}/#{param_value}/"
+ end
+ end
+ end
+ parent_prefix + association_prefix
+ end
+
+ # Add a parent-child relationship between +attribute+ and this
+ # class. This allows parameters like +attribute_id+ to contribute to
+ # generating nested urls.
+ def self.belongs_to(attribute = nil, options = {})
+ @belongs_to ||= []
+ if attribute
+ @belongs_to << Association::BelongsToAssociation.new(self, attribute, options)
+ end
+ @belongs_to
+ end
+
+ # Add a has_many relationship to another class.
+ def self.has_many(attribute = nil, options = {})
+ @has_many ||= []
+ if attribute
+ @has_many << Association::HasManyAssociation.new(self, attribute, options)
+ end
+ @has_many
+ end
+
+ # Add a has_one relationship to another class.
+ def self.has_one(attribute = nil, options = {})
+ @has_one ||= []
+ if attribute
+ @has_one << Association::HasOneAssociation.new(self, attribute, options)
+ end
+ @has_one
+ end
+
+ # merges in all of this class' associated classes' belongs_to
+ # associations, so we can handle deeply nested routes. So, for
+ # instance, if we have phone => address => lawyer, phone will look
+ # for address' belongs_to associations and merge them in. This
+ # allows us to have both lawyer_id and address_id at url generation
+ # time.
+ def self.belongs_to_with_parents
+ belongs_to.map(&:associated_attributes).flatten.uniq
+ end
+
+ def self.parents
+ @parents ||= belongs_to.map(&:associated_class)
+ end
+
+ end
+end
3  lib/reactive_resource/extensions.rb
@@ -0,0 +1,3 @@
+module Extensions
+ autoload :RelativeConstGet, 'reactive_resource/extensions/relative_const_get'
+end
26 lib/reactive_resource/extensions/relative_const_get.rb
@@ -0,0 +1,26 @@
+module Extensions
+ module RelativeConstGet
+
+ # Finds the constant with name +name+, relative to the calling
+ # module. For instance, <tt>A::B.const_get_relative("C")</tt> will
+ # search for A::C, then ::C. This is heavily inspired by
+ # +find_resource_in_modules+ in active_resource.
+ def relative_const_get(name)
+ module_names = self.name.split("::")
+ if module_names.length > 1
+ receiver = Object
+ namespaces = module_names[0, module_names.size-1].map do |module_name|
+ receiver = receiver.const_get(module_name)
+ end
+ const_args = RUBY_VERSION < "1.9" ? [name] : [name, false]
+ if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(*const_args) }
+ return namespace.const_get(*const_args)
+ else
+ raise NameError
+ end
+ else
+ const_get(name)
+ end
+ end
+ end
+end
3  lib/reactive_resource/version.rb
@@ -0,0 +1,3 @@
+module ReactiveResource
+ VERSION = "0.0.1"
+end
21 reactive_resource.gemspec
@@ -0,0 +1,21 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "reactive_resource/version"
+
+Gem::Specification.new do |s|
+ s.name = "reactive_resource"
+ s.version = ReactiveResource::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["TODO: Write your name"]
+ s.email = ["TODO: Write your email address"]
+ s.homepage = ""
+ s.summary = %q{TODO: Write a gem summary}
+ s.description = %q{TODO: Write a gem description}
+
+ s.rubyforge_project = "reactive_resource"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
1  test/unit/readme_test.rb
@@ -0,0 +1 @@
+puts "I do have tests for this, I just haven't moved them into this repo yet :-("
Please sign in to comment.
Something went wrong with that request. Please try again.