activeresource-five 5.0.0

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.
@@ -0,0 +1,15 @@
1
+ module ActiveResource
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def request(event)
4
+ result = event.payload[:result]
5
+ info "#{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}"
6
+ info "--> %d %s %d (%.1fms)" % [result.code, result.message, result.body.to_s.length, event.duration]
7
+ end
8
+
9
+ def logger
10
+ ActiveResource::Base.logger
11
+ end
12
+ end
13
+ end
14
+
15
+ ActiveResource::LogSubscriber.attach_to :active_resource
File without changes
@@ -0,0 +1,15 @@
1
+ require "active_resource"
2
+ require "rails"
3
+
4
+ module ActiveResource
5
+ class Railtie < Rails::Railtie
6
+ config.active_resource = ActiveSupport::OrderedOptions.new
7
+
8
+ initializer "active_resource.set_configs" do |app|
9
+ app.config.active_resource.each do |k,v|
10
+ ActiveResource::Base.send "#{k}=", v
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,77 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/core_ext/module/deprecation'
3
+
4
+ module ActiveResource
5
+ # = Active Resource reflection
6
+ #
7
+ # Associations in ActiveResource would be used to resolve nested attributes
8
+ # in a response with correct classes.
9
+ # Now they could be specified over Associations with the options :class_name
10
+ module Reflection # :nodoc:
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ class_attribute :reflections
15
+ self.reflections = {}
16
+ end
17
+
18
+ module ClassMethods
19
+ def create_reflection(macro, name, options)
20
+ reflection = AssociationReflection.new(macro, name, options)
21
+ self.reflections = self.reflections.merge(name => reflection)
22
+ reflection
23
+ end
24
+ end
25
+
26
+
27
+ class AssociationReflection
28
+
29
+ def initialize(macro, name, options)
30
+ @macro, @name, @options = macro, name, options
31
+ end
32
+
33
+ # Returns the name of the macro.
34
+ #
35
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
36
+ attr_reader :name
37
+
38
+ # Returns the macro type.
39
+ #
40
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
41
+ attr_reader :macro
42
+
43
+ # Returns the hash of options used for the macro.
44
+ #
45
+ # <tt>has_many :clients</tt> returns +{}+
46
+ attr_reader :options
47
+
48
+ # Returns the class for the macro.
49
+ #
50
+ # <tt>has_many :clients</tt> returns the Client class
51
+ def klass
52
+ @klass ||= class_name.constantize
53
+ end
54
+
55
+ # Returns the class name for the macro.
56
+ #
57
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
58
+ def class_name
59
+ @class_name ||= derive_class_name
60
+ end
61
+
62
+ # Returns the foreign_key for the macro.
63
+ def foreign_key
64
+ @foreign_key ||= self.options[:foreign_key] || "#{self.name.to_s.downcase}_id"
65
+ end
66
+
67
+ private
68
+ def derive_class_name
69
+ return (options[:class_name] ? options[:class_name].to_s.camelize : name.to_s.classify)
70
+ end
71
+
72
+ def derive_foreign_key
73
+ return options[:foreign_key] ? options[:foreign_key].to_s : "#{name.to_s.downcase}_id"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,57 @@
1
+ module ActiveResource # :nodoc:
2
+ class Schema # :nodoc:
3
+ # attributes can be known to be one of these types. They are easy to
4
+ # cast to/from.
5
+ KNOWN_ATTRIBUTE_TYPES = %w( string text integer float decimal datetime timestamp time date binary boolean )
6
+
7
+ # An array of attribute definitions, representing the attributes that
8
+ # have been defined.
9
+ attr_accessor :attrs
10
+
11
+ # The internals of an Active Resource Schema are very simple -
12
+ # unlike an Active Record TableDefinition (on which it is based).
13
+ # It provides a set of convenience methods for people to define their
14
+ # schema using the syntax:
15
+ # schema do
16
+ # string :foo
17
+ # integer :bar
18
+ # end
19
+ #
20
+ # The schema stores the name and type of each attribute. That is then
21
+ # read out by the schema method to populate the schema of the actual
22
+ # resource.
23
+ def initialize
24
+ @attrs = {}
25
+ end
26
+
27
+ def attribute(name, type, options = {})
28
+ raise ArgumentError, "Unknown Attribute type: #{type.inspect} for key: #{name.inspect}" unless type.nil? || Schema::KNOWN_ATTRIBUTE_TYPES.include?(type.to_s)
29
+
30
+ the_type = type.to_s
31
+ # TODO: add defaults
32
+ #the_attr = [type.to_s]
33
+ #the_attr << options[:default] if options.has_key? :default
34
+ @attrs[name.to_s] = the_type
35
+ self
36
+ end
37
+
38
+ # The following are the attribute types supported by Active Resource
39
+ # migrations.
40
+ KNOWN_ATTRIBUTE_TYPES.each do |attr_type|
41
+ # def string(*args)
42
+ # options = args.extract_options!
43
+ # attr_names = args
44
+ #
45
+ # attr_names.each { |name| attribute(name, 'string', options) }
46
+ # end
47
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
48
+ def #{attr_type.to_s}(*args)
49
+ options = args.extract_options!
50
+ attr_names = args
51
+
52
+ attr_names.each { |name| attribute(name, '#{attr_type}', options) }
53
+ end
54
+ EOV
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,114 @@
1
+ module ActiveResource
2
+ module Singleton
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ attr_writer :singleton_name
7
+
8
+ def singleton_name
9
+ @singleton_name ||= model_name.element
10
+ end
11
+
12
+ # Gets the singleton path for the object. If the +query_options+ parameter is omitted, Rails
13
+ # will split from the \prefix options.
14
+ #
15
+ # ==== Options
16
+ # * +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
17
+ # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
18
+ #
19
+ # * +query_options+ - A \hash to add items to the query string for the request.
20
+ #
21
+ # ==== Examples
22
+ # Weather.singleton_path
23
+ # # => /weather.json
24
+ #
25
+ # class Inventory < ActiveResource::Base
26
+ # self.site = "https://37s.sunrise.com"
27
+ # self.prefix = "/products/:product_id/"
28
+ # end
29
+ #
30
+ # Inventory.singleton_path(:product_id => 5)
31
+ # # => /products/5/inventory.json
32
+ #
33
+ # Inventory.singleton_path({:product_id => 5}, {:sold => true})
34
+ # # => /products/5/inventory.json?sold=true
35
+ #
36
+ def singleton_path(prefix_options = {}, query_options = nil)
37
+ check_prefix_options(prefix_options)
38
+
39
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
40
+ "#{prefix(prefix_options)}#{singleton_name}#{format_extension}#{query_string(query_options)}"
41
+ end
42
+
43
+ # Core method for finding singleton resources.
44
+ #
45
+ # ==== Arguments
46
+ # Takes a single argument of options
47
+ #
48
+ # ==== Options
49
+ # * <tt>:params</tt> - Sets the query and \prefix (nested URL) parameters.
50
+ #
51
+ # ==== Examples
52
+ # Weather.find
53
+ # # => GET /weather.json
54
+ #
55
+ # Weather.find(:params => {:degrees => 'fahrenheit'})
56
+ # # => GET /weather.json?degrees=fahrenheit
57
+ #
58
+ # == Failure or missing data
59
+ # A failure to find the requested object raises a ResourceNotFound exception.
60
+ #
61
+ # Inventory.find
62
+ # # => raises ResourceNotFound
63
+ def find(options={})
64
+ find_singleton(options)
65
+ end
66
+
67
+ private
68
+ # Find singleton resource
69
+ def find_singleton(options)
70
+ prefix_options, query_options = split_options(options[:params])
71
+
72
+ path = singleton_path(prefix_options, query_options)
73
+ resp = self.format.decode(self.connection.get(path, self.headers).body)
74
+ instantiate_record(resp, prefix_options)
75
+ end
76
+
77
+ end
78
+ # Deletes the resource from the remote service.
79
+ #
80
+ # ==== Examples
81
+ # weather = Weather.find
82
+ # weather.destroy
83
+ # Weather.find # 404 (Resource Not Found)
84
+ def destroy
85
+ connection.delete(singleton_path, self.class.headers)
86
+ end
87
+
88
+
89
+ protected
90
+
91
+ # Update the resource on the remote service
92
+ def update
93
+ connection.put(singleton_path(prefix_options), encode, self.class.headers).tap do |response|
94
+ load_attributes_from_response(response)
95
+ end
96
+ end
97
+
98
+ # Create (i.e. \save to the remote service) the \new resource.
99
+ def create
100
+ connection.post(singleton_path, encode, self.class.headers).tap do |response|
101
+ self.id = id_from_response(response)
102
+ load_attributes_from_response(response)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def singleton_path(options = nil)
109
+ self.class.singleton_path(options || prefix_options)
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_support/core_ext/object/duplicable'
2
+
3
+ module ThreadsafeAttributes
4
+ def self.included(klass)
5
+ klass.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def threadsafe_attribute(*attrs)
10
+ main_thread = Thread.main # remember this, because it could change after forking
11
+
12
+ attrs.each do |attr|
13
+ define_method attr do
14
+ get_threadsafe_attribute(attr, main_thread)
15
+ end
16
+
17
+ define_method "#{attr}=" do |value|
18
+ set_threadsafe_attribute(attr, value, main_thread)
19
+ end
20
+
21
+ define_method "#{attr}_defined?" do
22
+ threadsafe_attribute_defined?(attr, main_thread)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def get_threadsafe_attribute(name, main_thread)
31
+ if threadsafe_attribute_defined_by_thread?(name, Thread.current)
32
+ get_threadsafe_attribute_by_thread(name, Thread.current)
33
+ elsif threadsafe_attribute_defined_by_thread?(name, main_thread)
34
+ value = get_threadsafe_attribute_by_thread(name, main_thread)
35
+ value = value.dup if value.duplicable?
36
+ set_threadsafe_attribute_by_thread(name, value, Thread.current)
37
+ value
38
+ end
39
+ end
40
+
41
+ def set_threadsafe_attribute(name, value, main_thread)
42
+ set_threadsafe_attribute_by_thread(name, value, Thread.current)
43
+ unless threadsafe_attribute_defined_by_thread?(name, main_thread)
44
+ set_threadsafe_attribute_by_thread(name, value, main_thread)
45
+ end
46
+ end
47
+
48
+ def threadsafe_attribute_defined?(name, main_thread)
49
+ threadsafe_attribute_defined_by_thread?(name, Thread.current) || ((Thread.current != main_thread) && threadsafe_attribute_defined_by_thread?(name, main_thread))
50
+ end
51
+
52
+ def get_threadsafe_attribute_by_thread(name, thread)
53
+ thread["active.resource.#{name}.#{self.object_id}"]
54
+ end
55
+
56
+ def set_threadsafe_attribute_by_thread(name, value, thread)
57
+ thread["active.resource.#{name}.#{self.object_id}.defined"] = true
58
+ thread["active.resource.#{name}.#{self.object_id}"] = value
59
+ end
60
+
61
+ def threadsafe_attribute_defined_by_thread?(name, thread)
62
+ thread["active.resource.#{name}.#{self.object_id}.defined"]
63
+ end
64
+
65
+ end
@@ -0,0 +1,174 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+ require 'active_support/core_ext/object/blank'
3
+
4
+ module ActiveResource
5
+ class ResourceInvalid < ClientError #:nodoc:
6
+ end
7
+
8
+ # Active Resource validation is reported to and from this object, which is used by Base#save
9
+ # to determine whether the object in a valid state to be saved. See usage example in Validations.
10
+ class Errors < ActiveModel::Errors
11
+ # Grabs errors from an array of messages (like ActiveRecord::Validations).
12
+ # The second parameter directs the errors cache to be cleared (default)
13
+ # or not (by passing true).
14
+ def from_array(messages, save_cache = false)
15
+ clear unless save_cache
16
+ humanized_attributes = Hash[@base.known_attributes.map { |attr_name| [attr_name.humanize, attr_name] }]
17
+ messages.each do |message|
18
+ attr_message = humanized_attributes.keys.sort_by { |a| -a.length }.detect do |attr_name|
19
+ if message[0, attr_name.size + 1] == "#{attr_name} "
20
+ add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
21
+ end
22
+ end
23
+ self[:base] << message if attr_message.nil?
24
+ end
25
+ end
26
+
27
+ # Grabs errors from a hash of attribute => array of errors elements
28
+ # The second parameter directs the errors cache to be cleared (default)
29
+ # or not (by passing true)
30
+ #
31
+ # Unrecognized attribute names will be humanized and added to the record's
32
+ # base errors.
33
+ def from_hash(messages, save_cache = false)
34
+ clear unless save_cache
35
+
36
+ messages.each do |(key,errors)|
37
+ errors.each do |error|
38
+ if @base.known_attributes.include?(key)
39
+ add key, error
40
+ elsif key == 'base'
41
+ self[:base] << error
42
+ else
43
+ # reporting an error on an attribute not in attributes
44
+ # format and add them to base
45
+ self[:base] << "#{key.humanize} #{error}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # Grabs errors from a json response.
52
+ def from_json(json, save_cache = false)
53
+ decoded = ActiveSupport::JSON.decode(json) || {} rescue {}
54
+ if decoded.kind_of?(Hash) && (decoded.has_key?('errors') || decoded.empty?)
55
+ errors = decoded['errors'] || {}
56
+ if errors.kind_of?(Array)
57
+ # 3.2.1-style with array of strings
58
+ ActiveSupport::Deprecation.warn('Returning errors as an array of strings is deprecated.')
59
+ from_array errors, save_cache
60
+ else
61
+ # 3.2.2+ style
62
+ from_hash errors, save_cache
63
+ end
64
+ else
65
+ # <3.2-style respond_with - lacks 'errors' key
66
+ ActiveSupport::Deprecation.warn('Returning errors as a hash without a root "errors" key is deprecated.')
67
+ from_hash decoded, save_cache
68
+ end
69
+ end
70
+
71
+ # Grabs errors from an XML response.
72
+ def from_xml(xml, save_cache = false)
73
+ array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
74
+ from_array array, save_cache
75
+ end
76
+ end
77
+
78
+ # Module to support validation and errors with Active Resource objects. The module overrides
79
+ # Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned
80
+ # in the web service response. The module also adds an +errors+ collection that mimics the interface
81
+ # of the errors provided by ActiveModel::Errors.
82
+ #
83
+ # ==== Example
84
+ #
85
+ # Consider a Person resource on the server requiring both a +first_name+ and a +last_name+ with a
86
+ # <tt>validates_presence_of :first_name, :last_name</tt> declaration in the model:
87
+ #
88
+ # person = Person.new(:first_name => "Jim", :last_name => "")
89
+ # person.save # => false (server returns an HTTP 422 status code and errors)
90
+ # person.valid? # => false
91
+ # person.errors.empty? # => false
92
+ # person.errors.count # => 1
93
+ # person.errors.full_messages # => ["Last name can't be empty"]
94
+ # person.errors[:last_name] # => ["can't be empty"]
95
+ # person.last_name = "Halpert"
96
+ # person.save # => true (and person is now saved to the remote service)
97
+ #
98
+ module Validations
99
+ extend ActiveSupport::Concern
100
+ include ActiveModel::Validations
101
+
102
+ included do
103
+ alias_method :save_without_validation, :save
104
+ alias_method :save, :save_with_validation
105
+ end
106
+
107
+ # Validate a resource and save (POST) it to the remote web service.
108
+ # If any local validations fail - the save (POST) will not be attempted.
109
+ def save_with_validation(options={})
110
+ perform_validation = options[:validate] != false
111
+
112
+ # clear the remote validations so they don't interfere with the local
113
+ # ones. Otherwise we get an endless loop and can never change the
114
+ # fields so as to make the resource valid.
115
+ @remote_errors = nil
116
+ if perform_validation && valid? || !perform_validation
117
+ save_without_validation
118
+ true
119
+ else
120
+ false
121
+ end
122
+ rescue ResourceInvalid => error
123
+ # cache the remote errors because every call to <tt>valid?</tt> clears
124
+ # all errors. We must keep a copy to add these back after local
125
+ # validations.
126
+ @remote_errors = error
127
+ load_remote_errors(@remote_errors, true)
128
+ false
129
+ end
130
+
131
+
132
+ # Loads the set of remote errors into the object's Errors based on the
133
+ # content-type of the error-block received.
134
+ def load_remote_errors(remote_errors, save_cache = false ) #:nodoc:
135
+ case self.class.format
136
+ when ActiveResource::Formats[:xml]
137
+ errors.from_xml(remote_errors.response.body, save_cache)
138
+ when ActiveResource::Formats[:json]
139
+ errors.from_json(remote_errors.response.body, save_cache)
140
+ end
141
+ end
142
+
143
+ # Checks for errors on an object (i.e., is resource.errors empty?).
144
+ #
145
+ # Runs all the specified local validations and returns true if no errors
146
+ # were added, otherwise false.
147
+ # Runs local validations (eg those on your Active Resource model), and
148
+ # also any errors returned from the remote system the last time we
149
+ # saved.
150
+ # Remote errors can only be cleared by trying to re-save the resource.
151
+ #
152
+ # ==== Examples
153
+ # my_person = Person.create(params[:person])
154
+ # my_person.valid?
155
+ # # => true
156
+ #
157
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
158
+ # my_person.valid?
159
+ # # => false
160
+ #
161
+ def valid?
162
+ run_callbacks :validate do
163
+ super
164
+ load_remote_errors(@remote_errors, true) if defined?(@remote_errors) && @remote_errors.present?
165
+ errors.empty?
166
+ end
167
+ end
168
+
169
+ # Returns the Errors object that holds all information about attribute error messages.
170
+ def errors
171
+ @errors ||= Errors.new(self)
172
+ end
173
+ end
174
+ end