restly 0.0.1.alpha.16 → 0.0.1.alpha.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/README.md +2 -112
  2. data/lib/restly.rb +26 -1
  3. data/lib/restly/associations.rb +25 -12
  4. data/lib/restly/associations/base/conditionals.rb +2 -2
  5. data/lib/restly/base.rb +1 -0
  6. data/lib/restly/base/fields.rb +21 -17
  7. data/lib/restly/base/includes.rb +7 -2
  8. data/lib/restly/base/instance.rb +31 -12
  9. data/lib/restly/base/instance/actions.rb +2 -2
  10. data/lib/restly/base/instance/attributes.rb +46 -40
  11. data/lib/restly/base/instance/comparable.rb +13 -0
  12. data/lib/restly/base/instance/error_handling.rb +60 -0
  13. data/lib/restly/base/instance/persistence.rb +1 -1
  14. data/lib/restly/base/resource/specification/fields.rb +2 -2
  15. data/lib/restly/base/resource/specification/mass_assignment_security.rb +1 -1
  16. data/lib/restly/client.rb +20 -7
  17. data/lib/restly/collection.rb +48 -12
  18. data/lib/restly/collection/error_handling.rb +17 -0
  19. data/lib/restly/connection.rb +55 -11
  20. data/lib/restly/error.rb +5 -9
  21. data/lib/restly/middleware.rb +15 -2
  22. data/lib/restly/nested_attributes.rb +29 -23
  23. data/lib/restly/notifications.rb +65 -0
  24. data/lib/restly/version.rb +1 -1
  25. data/spec/restly/active_model_lint_spec.rb +11 -0
  26. data/spec/restly/associations/base/builders_spec.rb +8 -0
  27. data/spec/restly/associations/base/conditionals_spec.rb +8 -0
  28. data/spec/restly/associations/base/loaders_spec.rb +8 -0
  29. data/spec/restly/associations/base/modifiers_spec.rb +8 -0
  30. data/spec/restly/associations/base/stubs_spec.rb +8 -0
  31. data/spec/restly/associations/class_methods.rb +0 -0
  32. data/spec/restly/base/instance/comparable_spec.rb +8 -0
  33. metadata +22 -2
data/README.md CHANGED
@@ -16,118 +16,8 @@ Or install it yourself as:
16
16
 
17
17
  $ gem install restly
18
18
 
19
- ## Configuration
20
- In order for restly to funciton it will need some configuration. This can be done by placing restly.yml in your config directory. We have added a generator to make this easier.
21
-
22
- ```sh
23
- $ rails generate restly:configuration
24
- ```
25
-
26
- This will generate a file like so:
27
-
28
- ```yaml
29
- development: &development
30
- site: http://example.com # Change This
31
- default_format: json # defaults to: json
32
-
33
- ## Would you like requests to be cached?
34
- # cache: true
35
- # cache_options:
36
- # expires_in: 3600
37
-
38
- ## Enable Oauth?
39
- # use_oauth: true
40
- # oauth_options:
41
- # client_id: %client_id%
42
- # client_secret: %client_secret%
43
-
44
- test:
45
- <<: *development
46
-
47
- production:
48
- site: http://example.com # Change This
49
- default_format: json # defaults to: json
50
-
51
- # Would you like requests to be cached?
52
- cache: true
53
- cache_options:
54
- expires_in: 3600
55
-
56
- ## Enable Oauth?
57
- # use_oauth: true
58
- # oauth_options:
59
- # client_id: %client_id%
60
- # client_secret: %client_secret%
61
-
62
- ```
63
-
64
- =======
65
- *__Restly ships with OAuth2 support, allowing you to authorize requests before making them.__*
66
-
67
- ## Creating a Restly Model
68
-
69
- ### Using the generator
70
-
71
- ```
72
- $ rails generate restly:model MyModel
73
- ```
74
-
75
- This will generate a model that looks like this:
76
-
77
- ```ruby
78
- class MyModel < Restly::Base
79
-
80
- # self.resource_name = 'my_model'
81
- # self.path = 'some_path/to_resource' # defaults to: 'resource_name.pluralized'
82
-
83
- end
84
- ```
85
- =======
86
- Generates:
87
-
88
- ```
89
- class MyModel < Restly::Base
90
-
91
- field :id # implied
92
- field :my_attr
93
-
94
- end
95
- ```
96
-
97
- ## Interacting With Objects
98
-
99
- In restly interacting with objects is familiar.
100
-
101
- __Getting the Collection:__
102
-
103
- ```ruby
104
- MyModel.all
105
- ```
106
-
107
- __Finding an Instance:__
108
-
109
- ```ruby
110
- my_instance = MyModel.find(1)
111
- ```
112
-
113
- __Updating an Instance:__
114
-
115
- ```ruby
116
- my_instance.update_attributes({my_attr: 'val'})
117
- ```
118
-
119
- or
120
-
121
- ```ruby
122
- my_instance.my_attr = 'val'
123
- my_instance.save
124
- ```
125
-
126
- __Creating an Instance__
127
-
128
- ```
129
- my_instance
130
- ```
19
+ ## Getting Started
20
+ For information on how to use restly and how to get started, head over to the [Documentation](http://jwaldrip.github.com/restly).
131
21
 
132
22
  ## Contributing
133
23
 
@@ -30,4 +30,29 @@ module Restly
30
30
 
31
31
  end
32
32
 
33
- require 'restly/railtie' if Object.const_defined?('Rails')
33
+ require 'restly/railtie' if Object.const_defined?('Rails')
34
+ require 'restly/notifications'
35
+
36
+ class Object
37
+
38
+ @@__method_calls ||= {}
39
+
40
+ def current_method(index=0)
41
+ /`(?<curr_method>.*?)'/ =~ caller[index]
42
+ curr_method
43
+ end
44
+
45
+ def method_called(enum, *args)
46
+ count = enum.count
47
+ method = Digest::MD5.hexdigest( current_method(1) + args.to_sentence )
48
+
49
+ @@__method_calls.delete_if { |k, v| v[:timestamp] < Time.now - 10 }
50
+ @@__method_calls[method.to_sym] ||= {}
51
+ @@__method_calls[method.to_sym][:count] ||= 0
52
+ @@__method_calls[method.to_sym][:count] += 1
53
+ @@__method_calls[method.to_sym][:timestamp] = Time.now
54
+
55
+ @@__method_calls[method.to_sym][:count] >= count
56
+ end
57
+
58
+ end
@@ -56,7 +56,11 @@ module Restly::Associations
56
56
  private
57
57
 
58
58
  def association_attributes
59
- @association_attributes ||= {}.with_indifferent_access
59
+ @association_attributes ||= HashWithIndifferentAccess.new
60
+ end
61
+
62
+ def loaded_associations
63
+ @loaded_associations ||= HashWithIndifferentAccess.new
60
64
  end
61
65
 
62
66
  def set_association(attr, val)
@@ -66,19 +70,22 @@ module Restly::Associations
66
70
  end
67
71
 
68
72
  def get_association(attr, options={})
69
- association = self.class.reflect_on_resource_association(attr)
70
- if (stubbed = association.stub self, association_attributes[attr]).present?
71
- stubbed
72
- elsif (loaded = association.load self, options).present?
73
- loaded
74
- else
75
- association.build(self)
73
+ return loaded_associations[attr] if loaded_associations[attr].present?
74
+ ActiveSupport::Notifications.instrument("load_association.restly", model: self.class.name, association: attr) do
75
+ association = self.class.reflect_on_resource_association(attr)
76
+
77
+ loaded_associations[attr] = if (stubbed = association.stub self, association_attributes[attr]).present?
78
+ stubbed
79
+ elsif (loaded = association.load self, options).present?
80
+ loaded
81
+ else
82
+ association.build(self)
83
+ end
76
84
  end
77
-
78
85
  end
79
86
 
80
- def method_missing(m, *args, &block)
81
- if !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && associations.include?(m)
87
+ def association_missing(m, *args)
88
+ if !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && respond_to_association?(m)
82
89
  attr = attr.to_sym
83
90
  case !!setter
84
91
  when true
@@ -87,8 +94,14 @@ module Restly::Associations
87
94
  get_association(attr)
88
95
  end
89
96
  else
90
- super(m, *args, &block)
97
+ raise Restly::Error::InvalidAssociation, "Association is invalid"
91
98
  end
92
99
  end
93
100
 
101
+ def method_missing(m, *args, &block)
102
+ association_missing(m, *args)
103
+ rescue Restly::Error::InvalidAssociation
104
+ super
105
+ end
106
+
94
107
  end
@@ -2,8 +2,8 @@ module Restly::Associations::Base::Conditionals
2
2
 
3
3
  # Conditionals
4
4
  def valid?(val)
5
- valid_instances = Array.wrap(val).reject{ |item| item.resource_name == association_class.resource_name }.empty?
6
- raise Restly::Error::InvalidObject, "#{val} is not a #{association_class}" unless valid_instances
5
+ #valid_instances = Array.wrap(val).reject{ |item| item.resource_name == association_class.resource_name }.empty?
6
+ #raise Restly::Error::InvalidObject, "#{val} is not a #{association_class}" unless valid_instances
7
7
  end
8
8
 
9
9
  def collection?
@@ -65,6 +65,7 @@ module Restly
65
65
  self.resource_name = name.gsub(/.*::/,'').underscore if name.present?
66
66
  self.path = resource_name.pluralize
67
67
  self.params = params.dup
68
+ self.client = client.dup
68
69
  end
69
70
 
70
71
  # Run Active Support Load Hooks
@@ -5,7 +5,7 @@ module Restly::Base::Fields
5
5
  extend ClassMethods
6
6
 
7
7
  class_attribute :fields
8
- self.fields = FieldSet.new
8
+ self.fields = FieldSet.new(self)
9
9
  field :id
10
10
 
11
11
  inherited do
@@ -20,45 +20,49 @@ module Restly::Base::Fields
20
20
 
21
21
  def field(attr)
22
22
  if attr.is_a?(Symbol) || attr.is_a?(String)
23
- unless instance_method_already_implemented? attr
24
- define_attribute_method attr
25
- self.fields += [attr]
26
- end
23
+ define_attribute_method attr unless instance_method_already_implemented? attr
24
+ self.fields += [attr]
27
25
  else
28
26
  raise Restly::Error::InvalidField, "field must be a symbol or string."
29
27
  end
30
28
  end
31
29
 
32
30
  def exclude_field(name)
33
-
34
- # Remove from the class
35
31
  self.fields -= [name]
36
-
37
- # Remove from the instance
38
- before_initialize do
39
- self.fields -= [name]
40
- end
41
-
42
32
  end
43
33
 
44
34
  end
45
35
 
46
36
  class FieldSet < Set
47
37
 
38
+ attr_reader :owner
39
+
40
+ def initialize(owner, fields=[])
41
+ @owner = owner
42
+ super(fields)
43
+
44
+ self.each { |value| owner.send(:define_attribute_method, value) unless owner.send(:instance_method_already_implemented?, value.to_sym) }
45
+ end
46
+
48
47
  def include?(value)
49
48
  super(value.to_sym) || super(value.to_s)
50
49
  end
51
50
 
52
51
  def <<(value)
52
+ owner.send(:define_attribute_method, value) unless owner.send(:instance_method_already_implemented?, value.to_sym)
53
53
  super(value.to_sym)
54
54
  end
55
55
 
56
- def +(other_arry)
57
- super(other_arry.map(&:to_sym))
56
+ def +(other_array)
57
+ other_array.map!(&:to_sym)
58
+ other_array.each { |value| owner.send(:define_attribute_method, value) unless owner.send(:instance_method_already_implemented?, value.to_sym) }
59
+ super(other_array)
58
60
  end
59
61
 
60
- def -(other_arry)
61
- super(other_arry.map(&:to_sym))
62
+ def -(other_array)
63
+ other_array.map!(&:to_sym)
64
+ other_array.each { |value| owner.send(:define_attribute_method, value) if owner.send(:instance_method_already_implemented?, value.to_sym) }
65
+ super(other_array)
62
66
  end
63
67
 
64
68
  end
@@ -11,12 +11,17 @@ module Restly::Base::Includes
11
11
  delegate :site, :site=, :format, :format=, to: :client
12
12
 
13
13
  def client
14
- @client ||= Restly::Client.new
14
+ return @client if @client
15
+ @client = Restly::Client.new(nil, nil)
16
+ @client.resource = self
17
+ @client
15
18
  end
16
19
 
17
20
  def client=(client)
18
- raise Restly::Error::InvalidClient, "Client is invalid!"
21
+ raise Restly::Error::InvalidClient, "Client is invalid!" unless client.is_a?(Restly::Client)
19
22
  @client = client
23
+ @client.resource = self
24
+ @client
20
25
  end
21
26
 
22
27
  def has_specification
@@ -5,15 +5,19 @@ module Restly::Base::Instance
5
5
  autoload :Attributes
6
6
  autoload :Persistence
7
7
  autoload :WriteCallbacks
8
+ autoload :Comparable
9
+ autoload :ErrorHandling
8
10
 
9
11
  include Restly::Base::GenericMethods
10
12
  include Actions
11
13
  include Attributes
12
14
  include Persistence
13
15
  include WriteCallbacks
16
+ include Comparable
17
+ include ErrorHandling
14
18
 
15
19
  included do
16
- attr_reader :init_options, :response
20
+ attr_reader :init_options, :response, :errors
17
21
  delegate :spec, to: :resource
18
22
  end
19
23
 
@@ -22,25 +26,30 @@ module Restly::Base::Instance
22
26
  @init_options = options
23
27
  @attributes = HashWithIndifferentAccess.new
24
28
  @association_cache = {}
25
- @association_attributes = {}.with_indifferent_access
29
+ @association_attributes = HashWithIndifferentAccess.new
26
30
  @aggregation_cache = {}
27
31
  @attributes_cache = {}
28
32
  @previously_changed = {}
29
33
  @changed_attributes = {}
34
+ @errors = ActiveModel::Errors.new(self)
30
35
 
31
- run_callbacks :initialize do
32
- @readonly = options[:readonly] || false
33
- set_response options[:response] if options[:response]
34
- @loaded = options.has_key?(:loaded) ? options[:loaded] : true
35
- self.attributes = attributes if attributes
36
- self.connection = options[:connection] if options[:connection].is_a?(OAuth2::AccessToken)
36
+ ActiveSupport::Notifications.instrument("load_instance.restly", instance: self) do
37
37
 
38
- end
38
+ run_callbacks :initialize do
39
+
40
+ @readonly = options[:readonly] || false
41
+ set_response options[:response] if options[:response]
42
+ @loaded = options.has_key?(:loaded) ? options[:loaded] : true
43
+ self.attributes = attributes if attributes
44
+ self.connection = options[:connection] if options[:connection].is_a?(OAuth2::AccessToken)
45
+
46
+ end
39
47
 
48
+ end
40
49
  end
41
50
 
42
51
  def loaded?
43
- @loaded
52
+ !!@loaded
44
53
  end
45
54
 
46
55
  def connection
@@ -66,13 +75,21 @@ module Restly::Base::Instance
66
75
  end
67
76
  end
68
77
 
78
+ def to_param
79
+ id.to_s
80
+ end
81
+
69
82
  private
70
83
 
71
84
  def set_response(response)
72
85
  raise Restly::Error::InvalidResponse unless response.is_a? OAuth2::Response
73
86
  @response = response
74
87
  if response.try(:body)
75
- set_attributes_from_response
88
+ if response_has_errors?(response)
89
+ set_errors_from_response
90
+ else
91
+ set_attributes_from_response
92
+ end
76
93
  end
77
94
  end
78
95
 
@@ -80,7 +97,9 @@ module Restly::Base::Instance
80
97
  return {} unless response
81
98
  parsed = response.parsed || {}
82
99
  if parsed.is_a?(Hash) && parsed[resource_name]
83
- parsed[resource_name]
100
+ parsed[resource_name].with_indifferent_access
101
+ elsif parsed.is_a?(Hash)
102
+ parsed.with_indifferent_access
84
103
  else
85
104
  parsed
86
105
  end
@@ -25,13 +25,13 @@ module Restly::Base::Instance::Actions
25
25
 
26
26
  def update
27
27
  run_callbacks :update do
28
- set_attributes_from_response(connection.put path_with_format, body: @request_body, params: params)
28
+ set_response(connection.put path_with_format, body: @request_body, params: params)
29
29
  end
30
30
  end
31
31
 
32
32
  def create
33
33
  run_callbacks :create do
34
- set_attributes_from_response(connection.post path_with_format, body: @request_body, params: params)
34
+ set_response(connection.post path_with_format, body: @request_body, params: params)
35
35
  end
36
36
  end
37
37
 
@@ -1,5 +1,3 @@
1
- require "colorize"
2
-
3
1
  module Restly::Base::Instance::Attributes
4
2
 
5
3
  def update_attributes(attributes)
@@ -9,49 +7,35 @@ module Restly::Base::Instance::Attributes
9
7
 
10
8
  def attributes=(attributes)
11
9
  attributes.each do |k, v|
12
- write_attribute k, v
10
+ self[k] = v
13
11
  end
14
12
  end
15
13
 
16
14
  def attributes
17
15
  fields.reduce(HashWithIndifferentAccess.new) do |hash, key|
18
- hash[key] = read_attribute key, autoload: false
16
+ hash[key] = read_attribute(key, autoload: false)
19
17
  hash
20
18
  end
21
19
  end
22
20
 
23
- def write_attribute(attr, val)
24
- if fields.include?(attr)
25
- send("#{attr}_will_change!".to_sym) unless val == @attributes[attr.to_sym] || !@loaded
26
- @attributes[attr.to_sym] = Attribute.new(val)
27
-
28
- elsif (association = self.class.reflect_on_resource_association attr).present?
29
- set_association attr, association.stub(self, val) unless (@association_attributes ||= {}.with_indifferent_access)[attr].present?
30
-
31
- else
32
- puts "WARNING: Attribute `#{attr}` not written. ".colorize(:yellow) +
33
- "To fix this add the following the the model. -- field :#{attr}"
34
- end
21
+ def attribute(key, options={})
22
+ read_attribute(key)
35
23
  end
36
24
 
37
- def read_attribute(attr, options={})
38
- options.reverse_merge!({autoload: true})
39
- if @attributes[attr.to_sym].nil? && !!options[:autoload] && !loaded?
40
- load!
41
- read_attribute(attr)
42
- else
43
- @attributes[attr.to_sym]
44
- end
25
+ def []=(key, value)
26
+ send "#{key}=", value
45
27
  end
46
28
 
47
- alias :attribute :read_attribute
29
+ def [](key)
30
+ send key
31
+ end
48
32
 
49
33
  def has_attribute?(attr)
50
- attribute(attr)
34
+ fields.include?(attr) && attribute(attr)
51
35
  end
52
36
 
53
37
  def respond_to_attribute?(m)
54
- !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && associations.include?(attr)
38
+ !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && fields.include?(attr)
55
39
  end
56
40
 
57
41
  def respond_to?(m, include_private = false)
@@ -71,6 +55,41 @@ module Restly::Base::Instance::Attributes
71
55
 
72
56
  private
73
57
 
58
+ def attribute_missing(m, *args)
59
+ if !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && fields.include?(attr)
60
+ case !!setter
61
+ when true
62
+ write_attribute(attr, *args)
63
+ when false
64
+ read_attribute(attr, *args)
65
+ end
66
+ else
67
+ raise Restly::Error::InvalidAttribute, "Attribute does not exist!"
68
+ end
69
+ end
70
+
71
+ def method_missing(m, *args, &block)
72
+ attribute_missing(m, *args)
73
+ rescue Restly::Error::InvalidAttribute
74
+ super
75
+ end
76
+
77
+ def write_attribute(attr, val)
78
+ if fields.include?(attr)
79
+ send("#{attr}_will_change!".to_sym) unless val == @attributes[attr.to_sym] || !@loaded
80
+ @attributes[attr.to_sym] = Attribute.new(val)
81
+
82
+ else
83
+ ActiveSupport::Notifications.instrument("missing_attribute.restly", attr: attr)
84
+ end
85
+ end
86
+
87
+ def read_attribute(attr, options={})
88
+ options.reverse_merge!({ autoload: true })
89
+ load! if (key = attr.to_sym) != :id && @attributes[key].nil? && !!options[:autoload] && !loaded? && !exists?
90
+ @attributes[attr.to_sym]
91
+ end
92
+
74
93
  def attribute_for_inspect(attr_name)
75
94
  value = attribute(attr_name, autoload: false)
76
95
  if value.is_a?(String) && value.length > 50
@@ -84,19 +103,6 @@ module Restly::Base::Instance::Attributes
84
103
  self.attributes = parsed_response(response)
85
104
  end
86
105
 
87
- def method_missing(m, *args, &block)
88
- if !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && fields.include?(attr)
89
- case !!setter
90
- when true
91
- write_attribute(attr, *args)
92
- when false
93
- read_attribute(attr)
94
- end
95
- else
96
- super(m, *args, &block)
97
- end
98
- end
99
-
100
106
  class Attribute < Restly::Proxies::Base
101
107
 
102
108
  def initialize(attr)