morpheus 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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