morpheus 0.3.6 → 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +12 -0
- data/.rspec +1 -0
- data/README.md +77 -0
- data/Rakefile +16 -0
- data/lib/morpheus.rb +2 -2
- data/lib/morpheus/base.rb +1 -1
- data/lib/morpheus/client/inflections.rb +1 -1
- data/lib/morpheus/configuration.rb +41 -43
- data/lib/morpheus/errors.rb +1 -1
- data/lib/morpheus/mixins/associations.rb +3 -1
- data/lib/morpheus/mixins/attributes.rb +66 -67
- data/lib/morpheus/mixins/conversion.rb +9 -13
- data/lib/morpheus/mixins/filtering.rb +4 -1
- data/lib/morpheus/mixins/finders.rb +4 -1
- data/lib/morpheus/mixins/introspection.rb +12 -16
- data/lib/morpheus/mixins/persistence.rb +25 -26
- data/lib/morpheus/mixins/reflections.rb +4 -1
- data/lib/morpheus/mixins/request_handling.rb +4 -1
- data/lib/morpheus/mixins/response_parsing.rb +12 -12
- data/lib/morpheus/mixins/url_support.rb +4 -1
- data/lib/morpheus/request.rb +34 -34
- data/lib/morpheus/response.rb +2 -2
- data/lib/morpheus/response_parser.rb +9 -4
- data/lib/morpheus/version.rb +1 -1
- data/morpheus.gemspec +4 -4
- data/spec/dummy/app/resources/author.rb +0 -1
- data/spec/dummy/app/resources/book.rb +0 -1
- data/spec/dummy/app/resources/dog.rb +3 -3
- data/spec/dummy/app/resources/meeting.rb +1 -2
- data/spec/dummy/config/application.rb +7 -36
- data/spec/dummy/config/environments/production.rb +1 -1
- data/spec/dummy/config/initializers/morpheus.rb +1 -1
- data/spec/dummy/config/locales/en.yml +1 -1
- data/spec/dummy/config/routes.rb +0 -56
- data/spec/morpheus/associations/association_spec.rb +51 -33
- data/spec/morpheus/associations/belongs_to_association_spec.rb +14 -2
- data/spec/morpheus/associations/has_many_association_spec.rb +31 -11
- data/spec/morpheus/associations/has_one_association_spec.rb +14 -2
- data/spec/morpheus/base_spec.rb +104 -100
- data/spec/morpheus/client/associations_spec.rb +43 -36
- data/spec/morpheus/client/log_subscriber_spec.rb +33 -0
- data/spec/morpheus/client/railtie_spec.rb +5 -0
- data/spec/morpheus/configuration_spec.rb +92 -113
- data/spec/morpheus/mixins/associations_spec.rb +90 -108
- data/spec/morpheus/mixins/attributes_spec.rb +71 -77
- data/spec/morpheus/mixins/conversion_spec.rb +49 -59
- data/spec/morpheus/mixins/filtering_spec.rb +13 -0
- data/spec/morpheus/mixins/finders_spec.rb +180 -217
- data/spec/morpheus/mixins/introspection_spec.rb +81 -124
- data/spec/morpheus/mixins/persistence_spec.rb +140 -133
- data/spec/morpheus/mixins/{reflection_spec.rb → reflections_spec.rb} +28 -28
- data/spec/morpheus/mixins/request_handling_spec.rb +21 -0
- data/spec/morpheus/mixins/response_parsing_spec.rb +10 -2
- data/spec/morpheus/mixins/url_support_spec.rb +29 -0
- data/spec/morpheus/reflection_spec.rb +21 -0
- data/spec/morpheus/relation_spec.rb +34 -58
- data/spec/morpheus/request_cache_spec.rb +33 -2
- data/spec/morpheus/request_queue_spec.rb +37 -0
- data/spec/morpheus/request_spec.rb +102 -1
- data/spec/morpheus/response_parser_spec.rb +17 -0
- data/spec/morpheus/response_spec.rb +55 -51
- data/spec/morpheus/type_caster_spec.rb +128 -118
- data/spec/morpheus/url_builder_spec.rb +41 -0
- data/spec/shared/active_model_lint_test.rb +2 -2
- data/spec/spec_helper.rb +7 -14
- data/spec/support/configuration.rb +3 -4
- metadata +32 -16
- data/README.rdoc +0 -44
- data/autotest/discover.rb +0 -7
- data/lib/morpheus/mock.rb +0 -66
- data/spec/morpheus/mock_spec.rb +0 -133
data/.autotest
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Autotest.add_discovery { 'rspec2' }
|
2
|
+
|
3
|
+
Autotest.add_hook :initialize do |at|
|
4
|
+
at.clear_mappings
|
5
|
+
['.git', 'spec/dummy/log'].each { |exception| at.add_exception(exception) }
|
6
|
+
|
7
|
+
at.add_mapping(%r%^lib/(.*)\.rb$%) do |_, m|
|
8
|
+
at.files_matching %r%^spec/#{m[1]}_spec\.rb$%
|
9
|
+
end
|
10
|
+
|
11
|
+
at.add_mapping(%r%^spec/.*\_spec\.rb$%) { |filename, _| filename }
|
12
|
+
end
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Morpheus
|
2
|
+
## A RESTful API Client library built over Typhoeus
|
3
|
+
* The project is working toward a DSL that is very ActiveResource/ActiveRecord-like with some features stolen from other great libraries like DataMapper.
|
4
|
+
* We started working on this at a point when we felt we were pushing ActiveResource to its limits.
|
5
|
+
* We wanted a little more flexibility in terms of API coupling.
|
6
|
+
* We have plans to make Morpheus as flexible as possible in terms of query interface, and resource representation.
|
7
|
+
* Additionally, Morpheus is built on top of Typhoeus and will be building in intuitive interfaces for working with parallel requests.
|
8
|
+
|
9
|
+
```Ruby
|
10
|
+
class Dummy < Morpheus::Base
|
11
|
+
property :name
|
12
|
+
property :created_at
|
13
|
+
property :has_smarts
|
14
|
+
|
15
|
+
belongs_to :thingy
|
16
|
+
has_many :thingamabobs
|
17
|
+
has_one :doohickey
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
## Creating new resources
|
22
|
+
* Morpheus uses ActiveModel and so has an interface that tries to not stray too far from the similar ActiveRecord interface.
|
23
|
+
|
24
|
+
```Ruby
|
25
|
+
@dummy = Dummy.new(
|
26
|
+
:name => 'Dumb',
|
27
|
+
:has_smarts => false,
|
28
|
+
:thingy_id => 2) # => #<Dummy:0x1016858d8>
|
29
|
+
@dummy.new_record? # => true
|
30
|
+
@dummy.save # => POST http://example.com/dummies
|
31
|
+
```
|
32
|
+
|
33
|
+
## Finding existing resources
|
34
|
+
* Resources can be retrieved with as little as their classname and unique identifier.
|
35
|
+
|
36
|
+
```Ruby
|
37
|
+
@dummy = Dummy.find(1) # => GET http:example.com/dummies/1
|
38
|
+
@dummy.new_record? # => false
|
39
|
+
@dummy.name # => 'Dumb'
|
40
|
+
```
|
41
|
+
|
42
|
+
## Updating existing resources
|
43
|
+
* Resources can be updated just in the same way you would expect from a Active* library.
|
44
|
+
|
45
|
+
```Ruby
|
46
|
+
@dummy.name = 'Dumber' # => 'Dumber'
|
47
|
+
@dummy.save # => PUT http://example.com/dummies/1
|
48
|
+
@dummy.update_attributes(:has_smarts => true) # => PUT http://example.com/dummies/1
|
49
|
+
```
|
50
|
+
|
51
|
+
## Associations
|
52
|
+
* Morpheus was originally conceived at a point when we needed ActiveResource with a little more.
|
53
|
+
* Associations are a bit of that 'little more'.
|
54
|
+
|
55
|
+
```Ruby
|
56
|
+
@dummy.thingy # => GET http://example.com/thingies/2
|
57
|
+
@dummy.thingamabobs # => GET http://example.com/thingamabobs?dummy_id=1
|
58
|
+
@dummy.doohickey # => GET http://example.com/doohickey?dummy_id=1
|
59
|
+
```
|
60
|
+
|
61
|
+
## Query Interface
|
62
|
+
* ARel has a beautiful query interface.
|
63
|
+
* We have ported its method-chaining style to fit our query interface.
|
64
|
+
|
65
|
+
```Ruby
|
66
|
+
Dummy.all # => GET http://example.com/dummies
|
67
|
+
Dummy.find(1,2,3) # => GET http://example.com/dummies?ids=1,2,3
|
68
|
+
|
69
|
+
Dummy.where(:name => 'Dumb') # => GET http://example.com/dummies?name=Dumb
|
70
|
+
Dummy.where(:name => 'Dumb', :has_smarts => false) # => GET http://example.com/dummies?name=Dumb&has_smarts=false
|
71
|
+
Dummy.limit(1) # => GET http://example.com/dummies?limit=1
|
72
|
+
Dummy.result_per_page = 25 # => 25
|
73
|
+
Dummy.page(3) # => GET http://example.com/dummies?limit=25&offset=50
|
74
|
+
```
|
75
|
+
|
76
|
+
## More about the project
|
77
|
+
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
CHANGED
@@ -1,2 +1,18 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
2
|
require 'bundler/gem_tasks'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc 'Default: run specs.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc 'Run specs'
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
10
|
+
t.pattern = './spec/**/*_spec.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Generate code coverage'
|
14
|
+
RSpec::Core::RakeTask.new(:coverage) do |t|
|
15
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
16
|
+
t.rcov = true
|
17
|
+
t.rcov_opts = ['--exclude', 'spec']
|
18
|
+
end
|
data/lib/morpheus.rb
CHANGED
@@ -59,8 +59,8 @@ end
|
|
59
59
|
|
60
60
|
# The Railtie loads the LogSubscriber for printing output to the Rails log, and
|
61
61
|
# Associations which plug-in to ActiveRecord to link associated resources.
|
62
|
-
require 'morpheus/client/railtie'
|
63
|
-
require 'morpheus/client/inflections'
|
62
|
+
require 'morpheus/client/railtie' if defined?(Rails)
|
63
|
+
require 'morpheus/client/inflections' if defined?(ActiveSupport)
|
64
64
|
|
65
65
|
# There are some Typhoeus patches contained here.
|
66
66
|
require 'ext/typhoeus'
|
data/lib/morpheus/base.rb
CHANGED
@@ -1,48 +1,46 @@
|
|
1
1
|
module Morpheus
|
2
|
-
|
3
|
-
|
4
|
-
class << self
|
5
|
-
|
6
|
-
attr_accessor :username, :password
|
7
|
-
|
8
|
-
# Sets and gets the remote host service domain.
|
9
|
-
# The domain must be set before any requests can be made.
|
10
|
-
attr_writer :host
|
11
|
-
def host
|
12
|
-
@host || raise(Morpheus::ConfigurationError, "The request HOST has not been set. Please set the host using Morpheus::Configuration.host = 'http://www.example.com'")
|
13
|
-
end
|
14
|
-
|
15
|
-
# Links to the underlying libcurl request pool.
|
16
|
-
# Allows for concurrent requests.
|
17
|
-
def hydra
|
18
|
-
Typhoeus::Hydra.hydra
|
19
|
-
end
|
20
|
-
|
21
|
-
def hydra=(hydra)
|
22
|
-
Typhoeus::Hydra.hydra = hydra
|
23
|
-
end
|
24
|
-
|
25
|
-
# Handles the default logger that is used by the LogSubscriber
|
26
|
-
def logger
|
27
|
-
@logger ||= ::Logger.new(STDOUT)
|
28
|
-
end
|
29
|
-
|
30
|
-
def logger=(logger)
|
31
|
-
@logger = logger
|
32
|
-
end
|
33
|
-
|
34
|
-
# Can be set or unset to allow for test suite mocking.
|
35
|
-
def allow_net_connect=(allowed)
|
36
|
-
Typhoeus::Hydra.allow_net_connect = allowed
|
37
|
-
end
|
38
|
-
|
39
|
-
# Allow net connections by default.
|
40
|
-
allow_net_connect = true
|
41
|
-
|
42
|
-
def allow_net_connect?
|
43
|
-
Typhoeus::Hydra.allow_net_connect?
|
44
|
-
end
|
2
|
+
module Configuration
|
3
|
+
extend self
|
45
4
|
|
5
|
+
attr_accessor :username, :password
|
6
|
+
|
7
|
+
# Sets and gets the remote host service domain.
|
8
|
+
# The domain must be set before any requests can be made.
|
9
|
+
attr_writer :host
|
10
|
+
def host
|
11
|
+
@host || raise(Morpheus::ConfigurationError,
|
12
|
+
'The request HOST has not been set. Please set the host using Morpheus::Configuration.host = "http://www.example.com"')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Links to the underlying libcurl request pool.
|
16
|
+
# Allows for concurrent requests.
|
17
|
+
def hydra
|
18
|
+
Typhoeus::Hydra.hydra
|
19
|
+
end
|
20
|
+
|
21
|
+
def hydra=(hydra)
|
22
|
+
Typhoeus::Hydra.hydra = hydra
|
23
|
+
end
|
24
|
+
|
25
|
+
# Handles the default logger that is used by the LogSubscriber
|
26
|
+
def logger
|
27
|
+
@logger ||= ::Logger.new(STDOUT)
|
28
|
+
end
|
29
|
+
|
30
|
+
def logger=(logger)
|
31
|
+
@logger = logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# Can be set or unset to allow for test suite mocking.
|
35
|
+
def allow_net_connect=(allowed)
|
36
|
+
Typhoeus::Hydra.allow_net_connect = allowed
|
37
|
+
end
|
38
|
+
|
39
|
+
# Allow net connections by default.
|
40
|
+
allow_net_connect = true
|
41
|
+
|
42
|
+
def allow_net_connect?
|
43
|
+
Typhoeus::Hydra.allow_net_connect?
|
46
44
|
end
|
47
45
|
|
48
46
|
end
|
data/lib/morpheus/errors.rb
CHANGED
@@ -5,7 +5,9 @@ module Morpheus
|
|
5
5
|
autoload :HasManyAssociation, 'morpheus/associations/has_many_association'
|
6
6
|
autoload :HasOneAssociation, 'morpheus/associations/has_one_association'
|
7
7
|
|
8
|
-
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
9
11
|
|
10
12
|
module ClassMethods
|
11
13
|
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Morpheus
|
2
2
|
module Attributes
|
3
|
-
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
4
7
|
|
5
8
|
module ClassMethods
|
6
9
|
|
@@ -40,94 +43,90 @@ module Morpheus
|
|
40
43
|
|
41
44
|
end
|
42
45
|
|
43
|
-
|
46
|
+
def attributes
|
47
|
+
@attributes ||= self.class.default_attributes.dup
|
48
|
+
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
50
|
+
def read_attribute_for_validation(key)
|
51
|
+
attributes[key]
|
52
|
+
end
|
48
53
|
|
49
|
-
|
50
|
-
|
51
|
-
|
54
|
+
def typecast(attribute, value)
|
55
|
+
TypeCaster.cast(value, self.class.typecast_attributes[attribute.to_sym])
|
56
|
+
end
|
52
57
|
|
53
|
-
|
54
|
-
|
58
|
+
def attributes_without_basic_attributes
|
59
|
+
attributes_to_reject = %w( id errors valid )
|
60
|
+
self.class.reflections.keys.each do |key|
|
61
|
+
attributes_to_reject.push(key.to_s)
|
55
62
|
end
|
56
|
-
|
57
|
-
|
58
|
-
attributes_to_reject = %w( id errors valid )
|
59
|
-
self.class.reflections.keys.each do |key|
|
60
|
-
attributes_to_reject.push(key.to_s)
|
61
|
-
end
|
62
|
-
attributes.reject do |key, value|
|
63
|
-
attributes_to_reject.include?(key)
|
64
|
-
end
|
63
|
+
attributes.reject do |key, value|
|
64
|
+
attributes_to_reject.include?(key)
|
65
65
|
end
|
66
|
+
end
|
66
67
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
68
|
+
def update_attribute(attribute, value)
|
69
|
+
reflection = self.class.reflect_on_association(attribute)
|
70
|
+
if reflection
|
71
|
+
update_reflection(reflection, attribute, value)
|
72
|
+
else
|
73
|
+
attributes[attribute.to_sym] = typecast(attribute, value)
|
74
74
|
end
|
75
|
+
end
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
else
|
84
|
-
target = reflection.build_association(a_value)
|
85
|
-
end
|
86
|
-
association_object << target
|
87
|
-
end
|
88
|
-
elsif reflection.macro == :belongs_to
|
89
|
-
if value.instance_of? reflection.klass
|
90
|
-
target = value
|
77
|
+
def update_reflection(reflection, attribute, value)
|
78
|
+
return unless value
|
79
|
+
if reflection.macro == :has_many # need to construct each member of array one-by-one
|
80
|
+
association_object = send(attribute)
|
81
|
+
value.each do |a_value|
|
82
|
+
if a_value.instance_of? reflection.klass
|
83
|
+
target = a_value
|
91
84
|
else
|
92
|
-
|
93
|
-
polymorphic_class = send("#{reflection.name}_type".to_sym)
|
94
|
-
polymorphic_class = value['type'] if value.include?('type')
|
95
|
-
target = polymorphic_class.constantize.new(value)
|
96
|
-
else
|
97
|
-
target = reflection.build_association(value)
|
98
|
-
end
|
85
|
+
target = reflection.build_association(a_value)
|
99
86
|
end
|
100
|
-
|
87
|
+
association_object << target
|
88
|
+
end
|
89
|
+
elsif reflection.macro == :belongs_to
|
90
|
+
if value.instance_of? reflection.klass
|
91
|
+
target = value
|
101
92
|
else
|
102
|
-
if
|
103
|
-
|
93
|
+
if reflection.options[:polymorphic]
|
94
|
+
polymorphic_class = send("#{reflection.name}_type".to_sym)
|
95
|
+
polymorphic_class = value['type'] if value.include?('type')
|
96
|
+
target = polymorphic_class.constantize.new(value)
|
104
97
|
else
|
105
98
|
target = reflection.build_association(value)
|
106
99
|
end
|
107
|
-
send("#{attribute}=", target)
|
108
100
|
end
|
101
|
+
send("#{attribute}=", target)
|
102
|
+
else
|
103
|
+
if value.instance_of? reflection.klass
|
104
|
+
target = value
|
105
|
+
else
|
106
|
+
target = reflection.build_association(value)
|
107
|
+
end
|
108
|
+
send("#{attribute}=", target)
|
109
109
|
end
|
110
|
+
end
|
110
111
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
end
|
112
|
+
def merge_attributes(new_attributes)
|
113
|
+
new_attributes.each do |key, value|
|
114
|
+
case key.to_sym
|
115
|
+
when :errors
|
116
|
+
value.each do |k, v|
|
117
|
+
v.each do |message|
|
118
|
+
errors.add(k, message)
|
119
119
|
end
|
120
|
-
when :valid
|
121
|
-
@valid = value
|
122
|
-
else
|
123
|
-
update_attribute(key, value)
|
124
120
|
end
|
121
|
+
when :valid
|
122
|
+
@valid = value
|
123
|
+
else
|
124
|
+
update_attribute(key, value)
|
125
125
|
end
|
126
|
-
self
|
127
126
|
end
|
128
|
-
|
129
|
-
|
127
|
+
self
|
130
128
|
end
|
129
|
+
private :merge_attributes
|
131
130
|
|
132
131
|
end
|
133
132
|
end
|
@@ -1,21 +1,17 @@
|
|
1
1
|
module Morpheus
|
2
2
|
module Conversion
|
3
|
-
extend ActiveSupport::Concern
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
self
|
9
|
-
end
|
10
|
-
|
11
|
-
def to_param
|
12
|
-
id.to_s unless new_record?
|
13
|
-
end
|
4
|
+
def to_model
|
5
|
+
self
|
6
|
+
end
|
14
7
|
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
def to_param
|
9
|
+
id.to_s unless new_record?
|
10
|
+
end
|
18
11
|
|
12
|
+
def to_key
|
13
|
+
attributes.keys if persisted?
|
19
14
|
end
|
15
|
+
|
20
16
|
end
|
21
17
|
end
|