old_api_resource 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +26 -0
  4. data/Guardfile +22 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +49 -0
  8. data/VERSION +1 -0
  9. data/lib/old_api_resource.rb +70 -0
  10. data/lib/old_api_resource/associations.rb +192 -0
  11. data/lib/old_api_resource/associations/association_proxy.rb +92 -0
  12. data/lib/old_api_resource/associations/multi_object_proxy.rb +74 -0
  13. data/lib/old_api_resource/associations/related_object_hash.rb +12 -0
  14. data/lib/old_api_resource/associations/relation_scope.rb +24 -0
  15. data/lib/old_api_resource/associations/resource_scope.rb +25 -0
  16. data/lib/old_api_resource/associations/scope.rb +88 -0
  17. data/lib/old_api_resource/associations/single_object_proxy.rb +64 -0
  18. data/lib/old_api_resource/attributes.rb +162 -0
  19. data/lib/old_api_resource/base.rb +548 -0
  20. data/lib/old_api_resource/callbacks.rb +49 -0
  21. data/lib/old_api_resource/connection.rb +167 -0
  22. data/lib/old_api_resource/core_extensions.rb +7 -0
  23. data/lib/old_api_resource/custom_methods.rb +119 -0
  24. data/lib/old_api_resource/exceptions.rb +85 -0
  25. data/lib/old_api_resource/formats.rb +14 -0
  26. data/lib/old_api_resource/formats/json_format.rb +25 -0
  27. data/lib/old_api_resource/formats/xml_format.rb +36 -0
  28. data/lib/old_api_resource/log_subscriber.rb +15 -0
  29. data/lib/old_api_resource/mocks.rb +260 -0
  30. data/lib/old_api_resource/model_errors.rb +86 -0
  31. data/lib/old_api_resource/observing.rb +29 -0
  32. data/lib/old_api_resource/railtie.rb +18 -0
  33. data/old_api_resource.gemspec +134 -0
  34. data/spec/lib/associations_spec.rb +519 -0
  35. data/spec/lib/attributes_spec.rb +121 -0
  36. data/spec/lib/base_spec.rb +499 -0
  37. data/spec/lib/callbacks_spec.rb +68 -0
  38. data/spec/lib/mocks_spec.rb +28 -0
  39. data/spec/lib/model_errors_spec.rb +29 -0
  40. data/spec/spec_helper.rb +36 -0
  41. data/spec/support/mocks/association_mocks.rb +46 -0
  42. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  43. data/spec/support/mocks/test_resource_mocks.rb +43 -0
  44. data/spec/support/requests/association_requests.rb +14 -0
  45. data/spec/support/requests/error_resource_requests.rb +25 -0
  46. data/spec/support/requests/test_resource_requests.rb +31 -0
  47. data/spec/support/test_resource.rb +50 -0
  48. metadata +286 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format nested
3
+ --backtrace
data/Gemfile ADDED
@@ -0,0 +1,26 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem 'rails', '3.0.9'
6
+ gem 'hash_dealer'
7
+ gem 'rest-client'
8
+ gem 'log4r'
9
+
10
+ # Add dependencies to develop your gem here.
11
+ # Include everything needed to run rake, tests, features, etc.
12
+ group :development do
13
+ gem "rspec"
14
+ gem "ruby-debug19", :require => "ruby-debug"
15
+ gem "growl"
16
+ gem 'rspec-rails'
17
+ gem 'factory_girl'
18
+ gem 'simplecov'
19
+ gem 'faker'
20
+ gem 'guard'
21
+ gem 'guard-rspec'
22
+ gem 'mocha'
23
+ gem "bundler", "~> 1.0.0"
24
+ gem "jeweler", "~> 1.6.4"
25
+ gem "rcov", ">= 0"
26
+ end
data/Guardfile ADDED
@@ -0,0 +1,22 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2, :cli => "--color --format nested" do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch(%r{^lib/api_resource/(.+)\.rb$}) {|m| "spec/lib/#{m[1]}_spec.rb"}
8
+ watch('spec/spec_helper.rb') { "spec/" }
9
+
10
+ # Rails example
11
+ watch(%r{^spec/.+_spec\.rb$})
12
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
13
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
14
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
15
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec/" }
16
+ watch('spec/spec_helper.rb') { "spec/" }
17
+ watch('config/routes.rb') { "spec/routing" }
18
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
19
+ # Capybara request specs
20
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
21
+ end
22
+
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Ethan Langevin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = resource
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to resource
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Ethan Langevin. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "old_api_resource"
18
+ gem.homepage = "http://github.com/ejlangev/resource"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{A replacement for ActiveResource for RESTful APIs that handles associated object and multiple data sources}
21
+ gem.description = %Q{A replacement for ActiveResource for RESTful APIs that handles associated object and multiple data sources}
22
+ gem.email = "ejl6266@gmail.com"
23
+ gem.authors = ["Ethan Langevin"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "resource #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,70 @@
1
+ require 'active_support'
2
+ require 'active_support/inflector'
3
+ require 'active_support/core_ext/hash'
4
+ require 'active_support/core_ext/object'
5
+ require 'active_support/core_ext/class/attribute_accessors'
6
+ require 'active_support/core_ext/class/inheritable_attributes'
7
+ require 'old_api_resource/core_extensions'
8
+ require 'active_model'
9
+ require 'log4r'
10
+ require 'log4r/outputter/consoleoutputters'
11
+
12
+ require 'old_api_resource/exceptions'
13
+
14
+ module OldApiResource
15
+
16
+ extend ActiveSupport::Autoload
17
+
18
+ autoload :Associations
19
+ autoload :Attributes
20
+ autoload :Base
21
+ autoload :Callbacks
22
+ autoload :Connection
23
+ autoload :CustomMethods
24
+ autoload :Formats
25
+ autoload :Observing
26
+ autoload :Mocks
27
+ autoload :ModelErrors
28
+ autoload :Validations
29
+ autoload :LogSubscriber
30
+
31
+ mattr_writer :logger
32
+ mattr_accessor :raise_missing_definition_error; self.raise_missing_definition_error = false
33
+
34
+ # Load a fix for inflections for words ending in ess
35
+ ActiveSupport::Inflector.inflections do |inflect|
36
+ inflect.singular(/ess$/i, 'ess')
37
+ end
38
+
39
+ def self.load_mocks_and_factories
40
+ require 'hash_dealer'
41
+ Mocks.clear_endpoints
42
+ Mocks.init
43
+
44
+ Dir["#{File.dirname(__FILE__)}/../spec/support/requests/*.rb"].each {|f| require f}
45
+ Dir["#{File.dirname(__FILE__)}/../spec/support/**/*.rb"].each {|f| require f}
46
+ end
47
+
48
+ def self.site=(new_site)
49
+ OldApiResource::Base.site = new_site
50
+ end
51
+
52
+ def self.format=(new_format)
53
+ OldApiResource::Base.format = new_format
54
+ end
55
+ # logger
56
+ def self.logger
57
+ return @logger if @logger
58
+ @logger = Log4r::Logger.new("old_api_resource")
59
+ @logger.outputters = [Log4r::StdoutOutputter.new('console')]
60
+ @logger.level = Log4r::INFO
61
+ @logger
62
+ end
63
+
64
+ # Use this method to enable logging in the future
65
+ # def self.logging(val = nil)
66
+ # return (@@logging || false) unless val
67
+ # return @@logging = val
68
+ # end
69
+
70
+ end
@@ -0,0 +1,192 @@
1
+ require 'active_support'
2
+ require 'active_support/string_inquirer'
3
+ require 'old_api_resource/associations/relation_scope'
4
+ require 'old_api_resource/associations/resource_scope'
5
+ require 'old_api_resource/associations/multi_object_proxy'
6
+ require 'old_api_resource/associations/single_object_proxy'
7
+ require 'old_api_resource/associations/related_object_hash'
8
+
9
+ module OldApiResource
10
+
11
+ module Associations
12
+ extend ActiveSupport::Concern
13
+
14
+ ASSOCIATION_TYPES = [:belongs_to, :has_one, :has_many]
15
+
16
+ included do
17
+
18
+ class_inheritable_accessor :related_objects
19
+
20
+ # Hash to hold onto the definitions of the related objects
21
+ self.related_objects = RelatedObjectHash.new({
22
+ :belongs_to => RelatedObjectHash.new,
23
+ :has_one => RelatedObjectHash.new,
24
+ :has_many => RelatedObjectHash.new,
25
+ :scope => RelatedObjectHash.new
26
+ })
27
+ end
28
+
29
+
30
+ module ClassMethods
31
+
32
+ # Define the methods for creating and testing for associations, unfortunately
33
+ # scopes are different enough to require different methods :(
34
+ OldApiResource::Associations::ASSOCIATION_TYPES.each do |assoc|
35
+ self.module_eval <<-EOE, __FILE__, __LINE__ + 1
36
+ def #{assoc}(*args)
37
+ options = args.extract_options!
38
+ # Raise an error if we have multiple args and options
39
+ raise "Invalid arguments to #{assoc}" unless options.blank? || args.length == 1
40
+ args.each do |arg|
41
+ klass_name = (options[:class_name] ? options[:class_name].to_s.classify : arg.to_s.classify)
42
+ # add this to any descendants - the other methods etc are handled by inheritance
43
+ ([self] + self.descendants).each do |klass|
44
+ klass.related_objects[:#{assoc}][arg.to_sym] = klass_name
45
+ end
46
+ # We need to define reader and writer methods here
47
+ define_association_as_attribute(:#{assoc}, arg)
48
+ end
49
+ end
50
+
51
+ def #{assoc}?(name)
52
+ return self.related_objects[:#{assoc}][name.to_s.pluralize.to_sym].present? || self.related_objects[:#{assoc}][name.to_s.singularize.to_sym].present?
53
+ end
54
+
55
+ def #{assoc}_class_name(name)
56
+ raise "No such" + :#{assoc}.to_s + " association on #{name}" unless self.#{assoc}?(name)
57
+ return self.find_namespaced_class_name(self.related_objects[:#{assoc}][name.to_sym])
58
+ end
59
+
60
+ EOE
61
+ end
62
+
63
+ def scopes
64
+ return self.related_objects[:scope]
65
+ end
66
+
67
+ def scope(name, hsh)
68
+ raise ArgumentError, "Expecting an attributes hash given #{hsh.inspect}" unless hsh.is_a?(Hash)
69
+ self.related_objects[:scope][name.to_sym] = hsh
70
+ # we also need to define a class method for each scope
71
+ self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
72
+ def #{name}(opts = {})
73
+ return OldApiResource::Associations::ResourceScope.new(self, :#{name}, opts)
74
+ end
75
+ EOE
76
+ end
77
+
78
+ def scope?(name)
79
+ self.related_objects[:scope][name.to_sym].present?
80
+ end
81
+
82
+ def scope_attributes(name)
83
+ raise "No such scope #{name}" unless self.scope?(name)
84
+ self.related_objects[:scope][name.to_sym]
85
+ end
86
+
87
+ def association?(assoc)
88
+ self.related_objects.any? do |key, value|
89
+ next if key.to_s == "scope"
90
+ value.detect { |k,v| k.to_sym == assoc.to_sym }
91
+ end
92
+ end
93
+
94
+ def association_class_name(assoc)
95
+ raise ArgumentError, "#{assoc} is not a valid association of #{self}" unless self.association?(assoc)
96
+ result = self.related_objects.detect do |key,value|
97
+ ret = value.detect{|k,v| k.to_sym == assoc.to_sym }
98
+ return self.find_namespaced_class_name(ret[1]) if ret
99
+ end
100
+ end
101
+
102
+ def clear_associations
103
+ self.related_objects.each do |_, val|
104
+ val.clear
105
+ end
106
+ end
107
+
108
+ protected
109
+ def define_association_as_attribute(assoc_type, assoc_name)
110
+ define_attributes assoc_name
111
+ case assoc_type.to_sym
112
+ when :has_many
113
+ self.class_eval <<-EOE, __FILE__, __LINE__ + 1
114
+ def #{assoc_name}
115
+ self.attributes[:#{assoc_name}] ||= MultiObjectProxy.new(self.association_class_name('#{assoc_name}'), nil)
116
+ end
117
+ EOE
118
+ else
119
+ self.class_eval <<-EOE, __FILE__, __LINE__ + 1
120
+ def #{assoc_name}
121
+ self.attributes[:#{assoc_name}] ||= SingleObjectProxy.new(self.association_class_name('#{assoc_name}'), nil)
122
+ end
123
+ EOE
124
+ end
125
+ # Always define the setter the same
126
+ self.class_eval <<-EOE, __FILE__, __LINE__ + 1
127
+ def #{assoc_name}=(val)
128
+ #{assoc_name}_will_change! unless self.#{assoc_name}.internal_object == val
129
+ self.#{assoc_name}.internal_object = val
130
+ end
131
+ EOE
132
+ end
133
+
134
+ def find_namespaced_class_name(klass)
135
+ # return the name if it is itself namespaced
136
+ return klass if klass =~ /::/
137
+ ancestors = self.name.split("::")
138
+ if ancestors.size > 1
139
+ receiver = Object
140
+ namespaces = ancestors[0..-2].collect do |mod|
141
+ receiver = receiver.const_get(mod)
142
+ end
143
+ if namespace = namespaces.reverse.detect{|ns| ns.const_defined?(klass, false)}
144
+ return namespace.const_get(klass).name
145
+ end
146
+ end
147
+
148
+ return klass
149
+ end
150
+
151
+ end
152
+
153
+ module InstanceMethods
154
+ # For convenience we will define the methods for testing for the existence of an association
155
+ # and getting the class for an association as instance methods too to avoid tons of self.class calls
156
+ OldApiResource::Associations::ASSOCIATION_TYPES.each do |assoc|
157
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
158
+ def #{assoc}?(name)
159
+ return self.class.#{assoc}?(name)
160
+ end
161
+
162
+ def #{assoc}_class_name(name)
163
+ return self.class.#{assoc}_class_name(name)
164
+ end
165
+ EOE
166
+ end
167
+
168
+ def association?(assoc)
169
+ self.class.association?(assoc)
170
+ end
171
+
172
+ def association_class_name(assoc)
173
+ self.class.association_class_name(assoc)
174
+ end
175
+
176
+ def scopes
177
+ return self.class.scopes
178
+ end
179
+
180
+ def scope?(name)
181
+ return self.class.scope?(name)
182
+ end
183
+
184
+ def scope_attributes(name)
185
+ return self.class.scope_attributes(name)
186
+ end
187
+
188
+ end
189
+
190
+ end
191
+
192
+ end
@@ -0,0 +1,92 @@
1
+ module OldApiResource
2
+
3
+ module Associations
4
+
5
+ class AssociationProxy
6
+
7
+ cattr_accessor :remote_path_element; self.remote_path_element = :service_uri
8
+ cattr_accessor :include_class_scopes; self.include_class_scopes = true
9
+
10
+ attr_accessor :loaded, :klass, :internal_object, :remote_path, :scopes, :times_loaded
11
+
12
+ def initialize(klass_name, contents)
13
+ raise "Cannot create an association proxy to the unknown object #{klass_name}" unless defined?(klass_name.to_s.classify)
14
+ # A simple attr_accessor for testing purposes
15
+ self.times_loaded = 0
16
+ self.klass = klass_name.to_s.classify.constantize
17
+ self.load(contents)
18
+ self.loaded = {}.with_indifferent_access
19
+ if self.class.include_class_scopes
20
+ self.scopes = self.scopes.reverse_merge(self.klass.scopes)
21
+ end
22
+ # Now that we have set up all the scopes with the load method we need to create methods
23
+ self.scopes.each do |key, _|
24
+ self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
25
+ def #{key}(opts = {})
26
+ OldApiResource::Associations::RelationScope.new(self, :#{key}, opts)
27
+ end
28
+ EOE
29
+ end
30
+ end
31
+
32
+ def serializable_hash(options = {})
33
+ raise "Not Implemented: This method must be implemented in a subclass"
34
+ end
35
+
36
+ def scopes
37
+ @scopes ||= {}.with_indifferent_access
38
+ end
39
+
40
+ def scope?(scp)
41
+ self.scopes.keys.include?(scp.to_s)
42
+ end
43
+
44
+ def internal_object
45
+ @internal_object ||= self.load_scope_with_options(:all, {})
46
+ end
47
+
48
+ def method_missing(method, *args, &block)
49
+ self.internal_object.send(method, *args, &block)
50
+ end
51
+
52
+ def reload(scope = nil, opts = {})
53
+ if scope.nil?
54
+ self.loaded.clear
55
+ self.times_loaded = 0
56
+ # Remove the loaded object to force it to reload
57
+ remove_instance_variable(:@internal_object)
58
+ else
59
+ # Delete this key from the loaded hash which will cause it to be reloaded
60
+ self.loaded.delete(self.loaded_hash_key(scope, opts))
61
+ end
62
+ self
63
+ end
64
+
65
+ def to_s
66
+ self.internal_object.to_s
67
+ end
68
+
69
+ def inspect
70
+ self.internal_object.inspect
71
+ end
72
+
73
+ protected
74
+ # This method loads a particular scope with a set of options from the remote server
75
+ def load_scope_with_options(scope, options)
76
+ raise "Not Implemented: This method must be implemented in a subclass"
77
+ end
78
+ # This method is a helper to initialize for loading the data passed in to create this object
79
+ def load(contents)
80
+ raise "Not Implemented: This method must be implemented in a subclass"
81
+ end
82
+
83
+ # This method create the key for the loaded hash, it ensures that a unique set of scopes
84
+ # with a unique set of options is only loaded once
85
+ def loaded_hash_key(scope, options)
86
+ options.to_a.sort.inject(scope) {|accum,(k,v)| accum << "_#{k}_#{v}"}
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ end