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

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 (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)