activeresource-five 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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