morpheus 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/.rvmrc +1 -0
  2. data/Gemfile +21 -0
  3. data/Gemfile.lock +134 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.rdoc +44 -0
  6. data/Rakefile +48 -0
  7. data/VERSION +1 -0
  8. data/autotest/discover.rb +7 -0
  9. data/lib/ext/typhoeus.rb +37 -0
  10. data/lib/morpheus/associations/association.rb +110 -0
  11. data/lib/morpheus/associations/belongs_to_association.rb +45 -0
  12. data/lib/morpheus/associations/has_many_association.rb +70 -0
  13. data/lib/morpheus/associations/has_one_association.rb +46 -0
  14. data/lib/morpheus/base.rb +66 -0
  15. data/lib/morpheus/client/associations.rb +47 -0
  16. data/lib/morpheus/client/inflections.rb +3 -0
  17. data/lib/morpheus/client/log_subscriber.rb +64 -0
  18. data/lib/morpheus/client/railtie.rb +25 -0
  19. data/lib/morpheus/configuration.rb +49 -0
  20. data/lib/morpheus/errors.rb +32 -0
  21. data/lib/morpheus/filter.rb +18 -0
  22. data/lib/morpheus/mixins/associations.rb +55 -0
  23. data/lib/morpheus/mixins/attributes.rb +133 -0
  24. data/lib/morpheus/mixins/conversion.rb +21 -0
  25. data/lib/morpheus/mixins/filtering.rb +18 -0
  26. data/lib/morpheus/mixins/finders.rb +58 -0
  27. data/lib/morpheus/mixins/introspection.rb +25 -0
  28. data/lib/morpheus/mixins/persistence.rb +46 -0
  29. data/lib/morpheus/mixins/reflections.rb +24 -0
  30. data/lib/morpheus/mixins/request_handling.rb +34 -0
  31. data/lib/morpheus/mixins/response_parsing.rb +27 -0
  32. data/lib/morpheus/mixins/url_support.rb +36 -0
  33. data/lib/morpheus/mock.rb +66 -0
  34. data/lib/morpheus/reflection.rb +22 -0
  35. data/lib/morpheus/relation.rb +57 -0
  36. data/lib/morpheus/request.rb +41 -0
  37. data/lib/morpheus/request_cache.rb +18 -0
  38. data/lib/morpheus/request_queue.rb +44 -0
  39. data/lib/morpheus/response.rb +24 -0
  40. data/lib/morpheus/response_parser.rb +80 -0
  41. data/lib/morpheus/type_caster.rb +80 -0
  42. data/lib/morpheus/url_builder.rb +52 -0
  43. data/lib/morpheus.rb +64 -0
  44. data/morpheus.gemspec +191 -0
  45. data/spec/dummy/Rakefile +7 -0
  46. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  47. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  48. data/spec/dummy/app/models/purchase.rb +3 -0
  49. data/spec/dummy/app/resources/attendee.rb +2 -0
  50. data/spec/dummy/app/resources/author.rb +5 -0
  51. data/spec/dummy/app/resources/automobile.rb +6 -0
  52. data/spec/dummy/app/resources/book.rb +5 -0
  53. data/spec/dummy/app/resources/conference.rb +3 -0
  54. data/spec/dummy/app/resources/dog.rb +10 -0
  55. data/spec/dummy/app/resources/item.rb +5 -0
  56. data/spec/dummy/app/resources/meeting.rb +7 -0
  57. data/spec/dummy/app/resources/speaker.rb +3 -0
  58. data/spec/dummy/app/resources/state.rb +5 -0
  59. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  60. data/spec/dummy/config/application.rb +45 -0
  61. data/spec/dummy/config/boot.rb +10 -0
  62. data/spec/dummy/config/database.yml +22 -0
  63. data/spec/dummy/config/environment.rb +5 -0
  64. data/spec/dummy/config/environments/development.rb +26 -0
  65. data/spec/dummy/config/environments/production.rb +49 -0
  66. data/spec/dummy/config/environments/test.rb +35 -0
  67. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  68. data/spec/dummy/config/initializers/inflections.rb +10 -0
  69. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  70. data/spec/dummy/config/initializers/morpheus.rb +3 -0
  71. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  72. data/spec/dummy/config/initializers/session_store.rb +8 -0
  73. data/spec/dummy/config/locales/en.yml +5 -0
  74. data/spec/dummy/config/routes.rb +58 -0
  75. data/spec/dummy/config.ru +4 -0
  76. data/spec/dummy/db/migrate/20110605002144_create_purchases.rb +13 -0
  77. data/spec/dummy/db/test.sqlite3 +0 -0
  78. data/spec/dummy/public/404.html +26 -0
  79. data/spec/dummy/public/422.html +26 -0
  80. data/spec/dummy/public/500.html +26 -0
  81. data/spec/dummy/public/favicon.ico +0 -0
  82. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  83. data/spec/dummy/script/rails +6 -0
  84. data/spec/morpheus/associations/association_spec.rb +44 -0
  85. data/spec/morpheus/associations/belongs_to_association_spec.rb +5 -0
  86. data/spec/morpheus/associations/has_many_association_spec.rb +17 -0
  87. data/spec/morpheus/associations/has_one_association_spec.rb +5 -0
  88. data/spec/morpheus/base_spec.rb +126 -0
  89. data/spec/morpheus/client/associations_spec.rb +44 -0
  90. data/spec/morpheus/configuration_spec.rb +136 -0
  91. data/spec/morpheus/mixins/associations_spec.rb +141 -0
  92. data/spec/morpheus/mixins/attributes_spec.rb +99 -0
  93. data/spec/morpheus/mixins/conversion_spec.rb +76 -0
  94. data/spec/morpheus/mixins/finders_spec.rb +255 -0
  95. data/spec/morpheus/mixins/introspection_spec.rb +154 -0
  96. data/spec/morpheus/mixins/persistence_spec.rb +161 -0
  97. data/spec/morpheus/mixins/reflection_spec.rb +100 -0
  98. data/spec/morpheus/mixins/response_parsing_spec.rb +5 -0
  99. data/spec/morpheus/mock_spec.rb +133 -0
  100. data/spec/morpheus/relation_spec.rb +71 -0
  101. data/spec/morpheus/request_cache_spec.rb +5 -0
  102. data/spec/morpheus/request_spec.rb +5 -0
  103. data/spec/morpheus/response_spec.rb +73 -0
  104. data/spec/morpheus/type_caster_spec.rb +343 -0
  105. data/spec/shared/active_model_lint_test.rb +14 -0
  106. data/spec/spec_helper.rb +32 -0
  107. data/spec/support/configuration.rb +26 -0
  108. metadata +427 -0
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree-1.8.7-2010.02@morpheus
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'yajl-ruby', '~>0.8.2'
4
+ gem 'typhoeus', '~>0.2.4'
5
+ gem 'activemodel', '~>3.0.0'
6
+ gem 'activesupport', '~>3.0.0'
7
+ gem 'i18n', '~>0.5.0'
8
+
9
+ group :development, :test do
10
+ gem 'rails', '3.0.7'
11
+ gem 'sqlite3', '1.3.3'
12
+ gem 'rspec-rails', '2.6.0'
13
+ gem 'yard', '0.6.8'
14
+ gem 'jeweler', '1.6.0'
15
+ gem 'rcov', '0.9.9'
16
+ gem 'autotest', '4.4.6'
17
+ gem 'ZenTest', '4.6.2'
18
+ gem 'rocco', '0.7.0'
19
+ gem 'awesome_print', '0.4.0'
20
+ gem 'pry', '0.9.3'
21
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,134 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ ZenTest (4.6.2)
5
+ abstract (1.0.0)
6
+ actionmailer (3.0.7)
7
+ actionpack (= 3.0.7)
8
+ mail (~> 2.2.15)
9
+ actionpack (3.0.7)
10
+ activemodel (= 3.0.7)
11
+ activesupport (= 3.0.7)
12
+ builder (~> 2.1.2)
13
+ erubis (~> 2.6.6)
14
+ i18n (~> 0.5.0)
15
+ rack (~> 1.2.1)
16
+ rack-mount (~> 0.6.14)
17
+ rack-test (~> 0.5.7)
18
+ tzinfo (~> 0.3.23)
19
+ activemodel (3.0.7)
20
+ activesupport (= 3.0.7)
21
+ builder (~> 2.1.2)
22
+ i18n (~> 0.5.0)
23
+ activerecord (3.0.7)
24
+ activemodel (= 3.0.7)
25
+ activesupport (= 3.0.7)
26
+ arel (~> 2.0.2)
27
+ tzinfo (~> 0.3.23)
28
+ activeresource (3.0.7)
29
+ activemodel (= 3.0.7)
30
+ activesupport (= 3.0.7)
31
+ activesupport (3.0.7)
32
+ arel (2.0.10)
33
+ autotest (4.4.6)
34
+ ZenTest (>= 4.4.1)
35
+ awesome_print (0.4.0)
36
+ builder (2.1.2)
37
+ coderay (1.0.4)
38
+ diff-lcs (1.1.3)
39
+ erubis (2.6.6)
40
+ abstract (>= 1.0.0)
41
+ git (1.2.5)
42
+ i18n (0.5.0)
43
+ jeweler (1.6.0)
44
+ bundler (~> 1.0.0)
45
+ git (>= 1.2.5)
46
+ rake
47
+ mail (2.2.19)
48
+ activesupport (>= 2.3.6)
49
+ i18n (>= 0.4.0)
50
+ mime-types (~> 1.16)
51
+ treetop (~> 1.4.8)
52
+ method_source (0.6.7)
53
+ ruby_parser (>= 2.3.1)
54
+ mime-types (1.17.2)
55
+ mustache (0.99.4)
56
+ polyglot (0.3.3)
57
+ pry (0.9.3)
58
+ coderay (>= 0.9.8)
59
+ method_source (>= 0.6.0)
60
+ ruby_parser (>= 2.0.5)
61
+ slop (~> 1.9.0)
62
+ rack (1.2.4)
63
+ rack-mount (0.6.14)
64
+ rack (>= 1.0.0)
65
+ rack-test (0.5.7)
66
+ rack (>= 1.0)
67
+ rails (3.0.7)
68
+ actionmailer (= 3.0.7)
69
+ actionpack (= 3.0.7)
70
+ activerecord (= 3.0.7)
71
+ activeresource (= 3.0.7)
72
+ activesupport (= 3.0.7)
73
+ bundler (~> 1.0)
74
+ railties (= 3.0.7)
75
+ railties (3.0.7)
76
+ actionpack (= 3.0.7)
77
+ activesupport (= 3.0.7)
78
+ rake (>= 0.8.7)
79
+ thor (~> 0.14.4)
80
+ rake (0.9.2.2)
81
+ rcov (0.9.9)
82
+ rdiscount (1.6.8)
83
+ rocco (0.7)
84
+ mustache
85
+ rdiscount
86
+ rspec (2.6.0)
87
+ rspec-core (~> 2.6.0)
88
+ rspec-expectations (~> 2.6.0)
89
+ rspec-mocks (~> 2.6.0)
90
+ rspec-core (2.6.4)
91
+ rspec-expectations (2.6.0)
92
+ diff-lcs (~> 1.1.2)
93
+ rspec-mocks (2.6.0)
94
+ rspec-rails (2.6.0)
95
+ actionpack (~> 3.0)
96
+ activesupport (~> 3.0)
97
+ railties (~> 3.0)
98
+ rspec (~> 2.6.0)
99
+ ruby_parser (2.3.1)
100
+ sexp_processor (~> 3.0)
101
+ sexp_processor (3.0.7)
102
+ slop (1.9.1)
103
+ sqlite3 (1.3.3)
104
+ thor (0.14.6)
105
+ treetop (1.4.10)
106
+ polyglot
107
+ polyglot (>= 0.3.1)
108
+ typhoeus (0.2.4)
109
+ mime-types
110
+ mime-types
111
+ tzinfo (0.3.30)
112
+ yajl-ruby (0.8.3)
113
+ yard (0.6.8)
114
+
115
+ PLATFORMS
116
+ ruby
117
+
118
+ DEPENDENCIES
119
+ ZenTest (= 4.6.2)
120
+ activemodel (~> 3.0.0)
121
+ activesupport (~> 3.0.0)
122
+ autotest (= 4.4.6)
123
+ awesome_print (= 0.4.0)
124
+ i18n (~> 0.5.0)
125
+ jeweler (= 1.6.0)
126
+ pry (= 0.9.3)
127
+ rails (= 3.0.7)
128
+ rcov (= 0.9.9)
129
+ rocco (= 0.7.0)
130
+ rspec-rails (= 2.6.0)
131
+ sqlite3 (= 1.3.3)
132
+ typhoeus (~> 0.2.4)
133
+ yajl-ruby (~> 0.8.2)
134
+ yard (= 0.6.8)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Ryan Moran
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,44 @@
1
+ = RESTful API Client
2
+ Working toward a DSL that is very ActiveResource/ActiveRecord-like with some features stolen from other great libraries like DataMapper.
3
+
4
+ For Example:
5
+
6
+ class Dummy < PolarisResource::Base
7
+ property :name
8
+ property :created_at
9
+ property :has_smarts
10
+
11
+ belongs_to :thingy
12
+ has_many :thingamabobs
13
+ has_one :doohickey
14
+ end
15
+
16
+ @dummy = Dummy.new(
17
+ :name => "Dumb",
18
+ :has_smarts => false,
19
+ :thingy_id => 2) # => #<Dummy:0x1016858d8>
20
+ @dummy.new_record? # => true
21
+ @dummy.save # => POST http://localhost/dummies
22
+
23
+ @dummy = Dummy.find(1) # => GET http:localhost/dummies/1
24
+ @dummy.new_record? # => false
25
+ @dummy.name # => "Dumb"
26
+ @dummy.name = "Dumber" # => "Dumber"
27
+ @dummy.save # => PUT http://localhost/dummies/1
28
+ @dummy.update_attributes(:has_smarts => true) # => PUT http://localhost/dummies/1
29
+
30
+ @dummy.thingy # => GET http://localhost/thingies/2
31
+ @dummy.thingamabobs # => GET http://localhost/dummies/1/thingamabobs
32
+ @dummy.doohickey # => GET http://localhost/dummies/1/doohickey
33
+
34
+ Dummy.all # => GET http://localhost/dummies
35
+ Dummy.find(1,2,3) # => GET http://localhost/dummies?ids=1,2,3
36
+ Dummy.find([1,2,3]) # => GET http://localhost/dummies?ids=1,2,3
37
+
38
+ Dummy.where(:name => "Dumb") # => GET http://localhost/dummies?name=Dumb
39
+ Dummy.where(:name => "Dumb", :has_smarts => false) # => GET http://localhost/dummies?name=Dumb&has_smarts=false
40
+ Dummy.limit(1) # => GET http://localhost/dummies?limit=1
41
+ Dummy.result_per_page = 25 # => 25
42
+ Dummy.page(3) # => GET http://localhost/dummies?limit=25&offset=50
43
+
44
+ Design informed by Service-Oriented Design with Ruby and Rails by Paul Dix, @Amazon[http://www.amazon.com/Service-Oriented-Design-Rails-Addison-Wesley-Professional/dp/0321659368]
data/Rakefile ADDED
@@ -0,0 +1,48 @@
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 = "morpheus"
18
+ gem.homepage = "https://github.com/RevolutionPrep/morpheus"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{RESTful API Client}
21
+ gem.description = %Q{RESTful API Client}
22
+ gem.email = "ryan.moran@revolutionprep.com"
23
+ gem.authors = ["Ryan Moran"]
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_opts = "--exclude osx\/objc,gems\/,spec\/"
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'yard'
43
+ YARD::Rake::YardocTask.new
44
+
45
+ desc "Builds the documentation using Rocco"
46
+ task :doc do
47
+ system 'rocco -o doc -t doc/layout.mustache lib/*.rb lib/**/*.rb lib/**/**/*.rb'
48
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.4
@@ -0,0 +1,7 @@
1
+ Autotest.add_discovery do
2
+ 'rspec2'
3
+ end
4
+
5
+ Autotest.add_hook :initialize do |autotest|
6
+ ['.git', 'spec/dummy/log'].each { |exception| autotest.add_exception(exception) }
7
+ end
@@ -0,0 +1,37 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ module ConnectOptions
4
+
5
+ def check_allow_net_connect_with_error_wrapping!(request)
6
+ check_allow_net_connect_without_error_wrapping!(request)
7
+ rescue Typhoeus::Hydra::NetConnectNotAllowedError => ex
8
+ raise Morpheus::NetConnectNotAllowedError.new(request), ex.message
9
+ end
10
+ private :check_allow_net_connect_with_error_wrapping!
11
+
12
+ alias_method :check_allow_net_connect_without_error_wrapping!, :check_allow_net_connect!
13
+ alias_method :check_allow_net_connect!, :check_allow_net_connect_with_error_wrapping!
14
+
15
+ end
16
+
17
+ def response_from_easy(easy, request)
18
+ Morpheus::Response.new(
19
+ :code => easy.response_code,
20
+ :headers => easy.response_header,
21
+ :body => easy.response_body,
22
+ :time => easy.total_time_taken,
23
+ :start_transfer_time => easy.start_transfer_time,
24
+ :app_connect_time => easy.app_connect_time,
25
+ :pretransfer_time => easy.pretransfer_time,
26
+ :connect_time => easy.connect_time,
27
+ :name_lookup_time => easy.name_lookup_time,
28
+ :effective_url => easy.effective_url,
29
+ :curl_return_code => easy.curl_return_code,
30
+ :curl_error_message => easy.curl_error_message,
31
+ :request => request
32
+ )
33
+ end
34
+ private :response_from_easy
35
+
36
+ end
37
+ end
@@ -0,0 +1,110 @@
1
+ # The Association class serves as the basis for associating resources.
2
+ # Resources have an association DSL that follows that of ActiveRecord:
3
+ #
4
+ # has_many :automobiles
5
+ #
6
+ # has_one :car
7
+ #
8
+ # belongs_to :car_club
9
+ #
10
+ # This class acts as a proxy that will allow for lazy evaluation of an
11
+ # association. The proxy waits to make the request for the object until
12
+ # a method on the association is called. When this happens, because the
13
+ # method does not exist on the proxy object, method_missing will be
14
+ # called, the target object will be loaded, and then the method will be
15
+ # called on the loaded target.
16
+ module Morpheus
17
+ module Associations
18
+ class Association < ActiveSupport::BasicObject
19
+
20
+ # Associations can be loaded with several options.
21
+ def initialize(owner, association, settings = {})
22
+ # @owner stores the class that the association exists on.
23
+ @owner = owner
24
+
25
+ # @association stores the associated class, as named in the association
26
+ # method (i.e. :automobile, :car, :car_club)
27
+ @association = association
28
+
29
+ # @target stores the loaded object. It is not typically accessed directly,
30
+ # but instead should be accessed through the loaded_target method.
31
+ @target = settings[:target]
32
+
33
+ @filters = settings[:filters] || []
34
+
35
+ @includes = []
36
+
37
+ # @options holds the chosen options for the association. Several of these
38
+ # options are set in the subclass' initializer.
39
+ @options = settings[:options] || {}
40
+
41
+ # In some cases, the association name will not match that of the class
42
+ # that should be instantiated when it is invoked. Here, we can specify
43
+ # that this association uses a specified class as its target. When the
44
+ # request is made for the association, this class will be used to
45
+ # instantiate this object or collection.
46
+ @options[:class_name] = settings[:options][:class_name] || @association.to_s.classify
47
+ end
48
+
49
+ # The proxy implements a few methods that need to be delegated to the target
50
+ # so that they will work as expected.
51
+ def id
52
+ loaded_target.id
53
+ end
54
+
55
+ def nil?
56
+ loaded_target.nil?
57
+ end
58
+
59
+ def to_param
60
+ loaded_target.to_param
61
+ end
62
+
63
+ def try(method, *args, &block)
64
+ loaded_target.try(method, *args, &block)
65
+ end
66
+
67
+ def includes(*associations)
68
+ associations.each do |association|
69
+ @includes << association unless @includes.include?(association)
70
+ end
71
+ self
72
+ end
73
+
74
+ # This is left to be implemented by the subclasses as it will operate
75
+ # differently in each case.
76
+ def load_target!
77
+ end
78
+
79
+ private
80
+
81
+ # The loaded_target method holds a cached version of the loaded target.
82
+ # This method is used to access the proxied object. Since a request will
83
+ # be made when a method is invoked on the object, and this can happen
84
+ # very often, we are caching the target here, so that only a single
85
+ # request will be made.
86
+ def loaded_target
87
+ @target ||= load_target!
88
+ if Array === @target && !@filters.empty?
89
+ @filters.uniq.inject(@target.dup) do |target, filter|
90
+ filter.call(target)
91
+ end
92
+ else
93
+ @target
94
+ end
95
+ end
96
+
97
+ # The method_missing hook will be called when methods that do not exist
98
+ # on the proxy object are invoked. This is the point at which the proxied
99
+ # object is loaded, if it has not been loaded already.
100
+ def method_missing(m, *args, &block)
101
+ if filter = @association_class.find_filter(m)
102
+ with_filter(filter)
103
+ else
104
+ loaded_target.send(m, *args, &block)
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,45 @@
1
+ module Morpheus
2
+ module Associations
3
+ class BelongsToAssociation < Association
4
+
5
+ # The initializer calls out to the superclass' initializer, then will set the
6
+ # options particular to itself.
7
+ def initialize(owner, association, settings = {})
8
+ super
9
+
10
+ # The foreign key is used to generate the url for a request. The default uses
11
+ # the association name with an '_id' suffix as the generated key. For example,
12
+ # belongs_to :school, will have the foreign key :school_id.
13
+ @options[:foreign_key] ||= "#{@association.to_s}_id".to_sym
14
+
15
+ # The primary key defaults to :id.
16
+ @options[:primary_key] ||= :id
17
+
18
+ # Associations can be marked as polymorphic. These associations will use
19
+ # the returned type to instantiate the associated object.
20
+ @options[:polymorphic] = settings[:options][:polymorphic] || false
21
+
22
+ # @association_class stores the class of the association, constantized
23
+ # from the named association (i.e. Automobile, Car, CarClub)
24
+ @association_class = @options[:class_name].constantize
25
+ end
26
+
27
+ def with_filter(filter)
28
+ BelongsToAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
29
+ end
30
+
31
+ # When loading the target, the association will only be loaded if the foreign_key
32
+ # has been set. Additionally, the class used to find the record will be inferred
33
+ # by calling the method which is the name of the association with a '_type' suffix.
34
+ # Alternatively, the class name can be set by using the :class_name option.
35
+ def load_target!
36
+ if association_id = @owner.send(@options[:foreign_key])
37
+ polymorphic_class = @options[:polymorphic] ? @owner.send("#{@association}_type".to_sym).constantize : @options[:class_name].constantize
38
+ attributes = [UrlBuilder.belongs_to(polymorphic_class, association_id), nil, { :id => association_id }]
39
+ polymorphic_class.find(association_id)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,70 @@
1
+ module Morpheus
2
+ module Associations
3
+ class HasManyAssociation < Association
4
+
5
+ # The initializer calls out to the superclass' initializer and then
6
+ # sets the options particular to itself.
7
+ def initialize(owner, association, settings = {})
8
+ super
9
+
10
+ # The foreign key is used to generate the url for the association
11
+ # request when the association is transformed into a relation.
12
+ # The default is to use the class of the owner object with an '_id'
13
+ # suffix.
14
+ @options[:foreign_key] ||= "#{@owner.class.to_s.underscore}_id".to_sym
15
+
16
+ # The primary key is used in the generated url for the target. It
17
+ # defaults to :id.
18
+ @options[:primary_key] ||= :id
19
+
20
+ # @association_class stores the class of the association, constantized
21
+ # from the named association (i.e. Automobile, Car, CarClub)
22
+ @association_class = @options[:class_name].constantize
23
+ end
24
+
25
+ def with_filter(filter)
26
+ HasManyAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
27
+ end
28
+
29
+ # When loading the target, the primary key is first checked. If the
30
+ # key is nil, then an empty array is returned. Otherwise, the target
31
+ # is requested at the generated url. For a has_many :meetings
32
+ # association on a class called Course, the generated url might look
33
+ # like this: /meetings?course_id=1, where the 1 is the primary key.
34
+ def load_target!
35
+ if primary_key = @owner.send(@options[:primary_key])
36
+ Relation.new(@association.to_s.classify.constantize).where(@options[:foreign_key] => primary_key).all
37
+ else
38
+ []
39
+ end
40
+ end
41
+
42
+ # The where, limit, and page methods delegate to a Relation object.
43
+ # The association generates a relation, and then calls the very
44
+ # same where, limit, or page method on that relation object.
45
+ def where(query_attributes)
46
+ transform_association_into_relation.where(query_attributes)
47
+ end
48
+
49
+ def limit(amount)
50
+ transform_association_into_relation.limit(amount)
51
+ end
52
+
53
+ def page(page_number)
54
+ transform_association_into_relation.page(page_number)
55
+ end
56
+
57
+ def transform_association_into_relation
58
+ Relation.new(@association.to_s.classify.constantize).where(@options[:foreign_key] => @owner.send(@options[:primary_key]))
59
+ end
60
+ private :transform_association_into_relation
61
+
62
+ # The append operator is used to append new resources to the association.
63
+ def <<(one_of_many)
64
+ @target ||= []
65
+ @target << one_of_many
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,46 @@
1
+ module Morpheus
2
+ module Associations
3
+ class HasOneAssociation < Association
4
+
5
+ # The initializer calls out to the superclass' initializer and then
6
+ # sets the options particular to itself.
7
+ def initialize(owner, association, settings = {})
8
+ super
9
+
10
+ # The foreign key is used to generate the url for the association
11
+ # request when the association is transformed into a relation.
12
+ # The default is to use the class of the owner object with an '_id'
13
+ # suffix.
14
+ @options[:foreign_key] ||= "#{@owner.class.to_s.underscore}_id".to_sym
15
+
16
+ # The primary key is used in the generated url for the target. It
17
+ # defaults to :id.
18
+ @options[:primary_key] ||= :id
19
+
20
+ # @association_class stores the class of the association, constantized
21
+ # from the named association (i.e. Automobile, Car, CarClub)
22
+ if @options[:class_name]
23
+ @association_class = @options[:class_name].constantize
24
+ else
25
+ @association_class = @association
26
+ end
27
+ end
28
+
29
+ def with_filter(filter)
30
+ HasOneAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
31
+ end
32
+
33
+ # When loading the target, the primary key is first checked. If the
34
+ # key is nil, then an nil is returned. Otherwise, the target
35
+ # is requested at the generated url. For a has_one :meeting
36
+ # association on a class called Course, the generated url might look
37
+ # like this: /meetings?course_id=1, where the 1 is the primary key.
38
+ def load_target!
39
+ if primary_key = @owner.send(@options[:primary_key])
40
+ Relation.new(@association_class.to_s.classify.constantize).where(@options[:foreign_key] => primary_key).limit(1).first
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,66 @@
1
+ # Serving as the superclass to all resources, Morpheus::Base
2
+ # should be subclassed and extended from within your application.
3
+ # The Base class consists of the main components one would need to
4
+ # use the Morpheus API. The behavior of the class is contained
5
+ # in several module 'mix-ins'. Each of these modules will be explained
6
+ # within itself. Just know that the Base class includes these modules
7
+ # for you.
8
+ module Morpheus
9
+ class Base
10
+ extend ActiveModel::Naming
11
+ include ActiveModel::Validations
12
+ include ActiveModel::Dirty
13
+ include ActiveModel::Serializers::JSON
14
+ include ActiveModel::Serializers::Xml
15
+
16
+ # These modules wrap distinct behavior that is used within the Base class.
17
+ # In many cases they rely upon other classes in the library to complete
18
+ # their functionality. The helper classes can be used stand-alone, but
19
+ # are most effective when used as collaborators to the mix-in behaviors.
20
+ include Associations
21
+ include Attributes
22
+ include Conversion
23
+ include Filtering
24
+ include Finders
25
+ include Introspection
26
+ include Persistence
27
+ include Reflections
28
+ include RequestHandling
29
+ include ResponseParsing
30
+ include UrlSupport
31
+
32
+ # Defines a default 'id' property on all instances of Morpheus::Base and subclasses
33
+ property :id, :integer
34
+
35
+ def initialize(new_attributes = {})
36
+ new_attributes = HashWithIndifferentAccess.new(new_attributes)
37
+ @errors = ActiveModel::Errors.new(self)
38
+ new_attributes.each do |attribute, value|
39
+ self.class.default_attributes.store(attribute.to_sym, nil) unless self.class.attribute_defined?(attribute)
40
+ update_attribute(attribute, value)
41
+ end
42
+ end
43
+
44
+ def ==(comparison_object)
45
+ comparison_object.equal?(self) ||
46
+ (comparison_object.instance_of?(self.class) && comparison_object.id == id && !comparison_object.new_record?)
47
+ end
48
+
49
+ def self.base_class
50
+ self
51
+ end
52
+
53
+ private
54
+
55
+ def method_missing(m, *args, &block)
56
+ if attributes.keys.include?(m.to_s.delete('='))
57
+ # Defines attribute accessors when the missing method can be found in the attributes hash
58
+ self.class.send(:define_attribute_accessor, m)
59
+ send(m, *args, &block)
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ end
66
+ end