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.
- data/.rvmrc +1 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +134 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +44 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +7 -0
- data/lib/ext/typhoeus.rb +37 -0
- data/lib/morpheus/associations/association.rb +110 -0
- data/lib/morpheus/associations/belongs_to_association.rb +45 -0
- data/lib/morpheus/associations/has_many_association.rb +70 -0
- data/lib/morpheus/associations/has_one_association.rb +46 -0
- data/lib/morpheus/base.rb +66 -0
- data/lib/morpheus/client/associations.rb +47 -0
- data/lib/morpheus/client/inflections.rb +3 -0
- data/lib/morpheus/client/log_subscriber.rb +64 -0
- data/lib/morpheus/client/railtie.rb +25 -0
- data/lib/morpheus/configuration.rb +49 -0
- data/lib/morpheus/errors.rb +32 -0
- data/lib/morpheus/filter.rb +18 -0
- data/lib/morpheus/mixins/associations.rb +55 -0
- data/lib/morpheus/mixins/attributes.rb +133 -0
- data/lib/morpheus/mixins/conversion.rb +21 -0
- data/lib/morpheus/mixins/filtering.rb +18 -0
- data/lib/morpheus/mixins/finders.rb +58 -0
- data/lib/morpheus/mixins/introspection.rb +25 -0
- data/lib/morpheus/mixins/persistence.rb +46 -0
- data/lib/morpheus/mixins/reflections.rb +24 -0
- data/lib/morpheus/mixins/request_handling.rb +34 -0
- data/lib/morpheus/mixins/response_parsing.rb +27 -0
- data/lib/morpheus/mixins/url_support.rb +36 -0
- data/lib/morpheus/mock.rb +66 -0
- data/lib/morpheus/reflection.rb +22 -0
- data/lib/morpheus/relation.rb +57 -0
- data/lib/morpheus/request.rb +41 -0
- data/lib/morpheus/request_cache.rb +18 -0
- data/lib/morpheus/request_queue.rb +44 -0
- data/lib/morpheus/response.rb +24 -0
- data/lib/morpheus/response_parser.rb +80 -0
- data/lib/morpheus/type_caster.rb +80 -0
- data/lib/morpheus/url_builder.rb +52 -0
- data/lib/morpheus.rb +64 -0
- data/morpheus.gemspec +191 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/purchase.rb +3 -0
- data/spec/dummy/app/resources/attendee.rb +2 -0
- data/spec/dummy/app/resources/author.rb +5 -0
- data/spec/dummy/app/resources/automobile.rb +6 -0
- data/spec/dummy/app/resources/book.rb +5 -0
- data/spec/dummy/app/resources/conference.rb +3 -0
- data/spec/dummy/app/resources/dog.rb +10 -0
- data/spec/dummy/app/resources/item.rb +5 -0
- data/spec/dummy/app/resources/meeting.rb +7 -0
- data/spec/dummy/app/resources/speaker.rb +3 -0
- data/spec/dummy/app/resources/state.rb +5 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +22 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +26 -0
- data/spec/dummy/config/environments/production.rb +49 -0
- data/spec/dummy/config/environments/test.rb +35 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/morpheus.rb +3 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20110605002144_create_purchases.rb +13 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/stylesheets/.gitkeep +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/morpheus/associations/association_spec.rb +44 -0
- data/spec/morpheus/associations/belongs_to_association_spec.rb +5 -0
- data/spec/morpheus/associations/has_many_association_spec.rb +17 -0
- data/spec/morpheus/associations/has_one_association_spec.rb +5 -0
- data/spec/morpheus/base_spec.rb +126 -0
- data/spec/morpheus/client/associations_spec.rb +44 -0
- data/spec/morpheus/configuration_spec.rb +136 -0
- data/spec/morpheus/mixins/associations_spec.rb +141 -0
- data/spec/morpheus/mixins/attributes_spec.rb +99 -0
- data/spec/morpheus/mixins/conversion_spec.rb +76 -0
- data/spec/morpheus/mixins/finders_spec.rb +255 -0
- data/spec/morpheus/mixins/introspection_spec.rb +154 -0
- data/spec/morpheus/mixins/persistence_spec.rb +161 -0
- data/spec/morpheus/mixins/reflection_spec.rb +100 -0
- data/spec/morpheus/mixins/response_parsing_spec.rb +5 -0
- data/spec/morpheus/mock_spec.rb +133 -0
- data/spec/morpheus/relation_spec.rb +71 -0
- data/spec/morpheus/request_cache_spec.rb +5 -0
- data/spec/morpheus/request_spec.rb +5 -0
- data/spec/morpheus/response_spec.rb +73 -0
- data/spec/morpheus/type_caster_spec.rb +343 -0
- data/spec/shared/active_model_lint_test.rb +14 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/configuration.rb +26 -0
- 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
|
data/lib/ext/typhoeus.rb
ADDED
|
@@ -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
|