cardiac 0.2.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/LICENSE +22 -0
- data/Rakefile +66 -0
- data/cardiac-0.2.0.pre2.gem +0 -0
- data/cardiac.gemspec +48 -0
- data/lib/cardiac/declarations.rb +70 -0
- data/lib/cardiac/errors.rb +65 -0
- data/lib/cardiac/log_subscriber.rb +55 -0
- data/lib/cardiac/model/attributes.rb +146 -0
- data/lib/cardiac/model/base.rb +161 -0
- data/lib/cardiac/model/callbacks.rb +47 -0
- data/lib/cardiac/model/declarations.rb +106 -0
- data/lib/cardiac/model/dirty.rb +117 -0
- data/lib/cardiac/model/locale/en.yml +7 -0
- data/lib/cardiac/model/operations.rb +49 -0
- data/lib/cardiac/model/persistence.rb +171 -0
- data/lib/cardiac/model/querying.rb +129 -0
- data/lib/cardiac/model/validations.rb +124 -0
- data/lib/cardiac/model.rb +17 -0
- data/lib/cardiac/operation_builder.rb +75 -0
- data/lib/cardiac/operation_handler.rb +215 -0
- data/lib/cardiac/railtie.rb +20 -0
- data/lib/cardiac/reflections.rb +85 -0
- data/lib/cardiac/representation.rb +124 -0
- data/lib/cardiac/resource/adapter.rb +178 -0
- data/lib/cardiac/resource/builder.rb +107 -0
- data/lib/cardiac/resource/codec_methods.rb +58 -0
- data/lib/cardiac/resource/config_methods.rb +39 -0
- data/lib/cardiac/resource/extension_methods.rb +115 -0
- data/lib/cardiac/resource/request_methods.rb +138 -0
- data/lib/cardiac/resource/subresource.rb +88 -0
- data/lib/cardiac/resource/uri_methods.rb +176 -0
- data/lib/cardiac/resource.rb +77 -0
- data/lib/cardiac/util.rb +120 -0
- data/lib/cardiac/version.rb +3 -0
- data/lib/cardiac.rb +61 -0
- data/spec/rails-3.2/Gemfile +9 -0
- data/spec/rails-3.2/Gemfile.lock +136 -0
- data/spec/rails-3.2/Rakefile +10 -0
- data/spec/rails-3.2/app_root/app/assets/javascripts/application.js +15 -0
- data/spec/rails-3.2/app_root/app/assets/stylesheets/application.css +13 -0
- data/spec/rails-3.2/app_root/app/controllers/application_controller.rb +3 -0
- data/spec/rails-3.2/app_root/app/helpers/application_helper.rb +2 -0
- data/spec/rails-3.2/app_root/app/views/layouts/application.html.erb +14 -0
- data/spec/rails-3.2/app_root/config/application.rb +29 -0
- data/spec/rails-3.2/app_root/config/boot.rb +13 -0
- data/spec/rails-3.2/app_root/config/database.yml +25 -0
- data/spec/rails-3.2/app_root/config/environment.rb +5 -0
- data/spec/rails-3.2/app_root/config/environments/development.rb +10 -0
- data/spec/rails-3.2/app_root/config/environments/production.rb +11 -0
- data/spec/rails-3.2/app_root/config/environments/test.rb +11 -0
- data/spec/rails-3.2/app_root/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails-3.2/app_root/config/initializers/inflections.rb +15 -0
- data/spec/rails-3.2/app_root/config/initializers/mime_types.rb +5 -0
- data/spec/rails-3.2/app_root/config/initializers/secret_token.rb +7 -0
- data/spec/rails-3.2/app_root/config/initializers/session_store.rb +8 -0
- data/spec/rails-3.2/app_root/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails-3.2/app_root/config/locales/en.yml +5 -0
- data/spec/rails-3.2/app_root/config/routes.rb +2 -0
- data/spec/rails-3.2/app_root/db/test.sqlite3 +0 -0
- data/spec/rails-3.2/app_root/log/test.log +2403 -0
- data/spec/rails-3.2/app_root/public/404.html +26 -0
- data/spec/rails-3.2/app_root/public/422.html +26 -0
- data/spec/rails-3.2/app_root/public/500.html +25 -0
- data/spec/rails-3.2/app_root/public/favicon.ico +0 -0
- data/spec/rails-3.2/app_root/script/rails +6 -0
- data/spec/rails-3.2/spec/spec_helper.rb +25 -0
- data/spec/rails-4.0/Gemfile +9 -0
- data/spec/rails-4.0/Gemfile.lock +132 -0
- data/spec/rails-4.0/Rakefile +10 -0
- data/spec/rails-4.0/app_root/app/assets/javascripts/application.js +15 -0
- data/spec/rails-4.0/app_root/app/assets/stylesheets/application.css +13 -0
- data/spec/rails-4.0/app_root/app/controllers/application_controller.rb +3 -0
- data/spec/rails-4.0/app_root/app/helpers/application_helper.rb +2 -0
- data/spec/rails-4.0/app_root/app/views/layouts/application.html.erb +14 -0
- data/spec/rails-4.0/app_root/config/application.rb +28 -0
- data/spec/rails-4.0/app_root/config/boot.rb +13 -0
- data/spec/rails-4.0/app_root/config/database.yml +25 -0
- data/spec/rails-4.0/app_root/config/environment.rb +5 -0
- data/spec/rails-4.0/app_root/config/environments/development.rb +9 -0
- data/spec/rails-4.0/app_root/config/environments/production.rb +11 -0
- data/spec/rails-4.0/app_root/config/environments/test.rb +10 -0
- data/spec/rails-4.0/app_root/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails-4.0/app_root/config/initializers/inflections.rb +15 -0
- data/spec/rails-4.0/app_root/config/initializers/mime_types.rb +5 -0
- data/spec/rails-4.0/app_root/config/initializers/secret_token.rb +7 -0
- data/spec/rails-4.0/app_root/config/initializers/session_store.rb +8 -0
- data/spec/rails-4.0/app_root/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails-4.0/app_root/config/locales/en.yml +5 -0
- data/spec/rails-4.0/app_root/config/routes.rb +2 -0
- data/spec/rails-4.0/app_root/db/test.sqlite3 +0 -0
- data/spec/rails-4.0/app_root/log/development.log +50 -0
- data/spec/rails-4.0/app_root/log/test.log +2399 -0
- data/spec/rails-4.0/app_root/public/404.html +26 -0
- data/spec/rails-4.0/app_root/public/422.html +26 -0
- data/spec/rails-4.0/app_root/public/500.html +25 -0
- data/spec/rails-4.0/app_root/public/favicon.ico +0 -0
- data/spec/rails-4.0/app_root/script/rails +6 -0
- data/spec/rails-4.0/spec/spec_helper.rb +25 -0
- data/spec/shared/cardiac/declarations_spec.rb +103 -0
- data/spec/shared/cardiac/model/base_spec.rb +446 -0
- data/spec/shared/cardiac/operation_builder_spec.rb +96 -0
- data/spec/shared/cardiac/operation_handler_spec.rb +82 -0
- data/spec/shared/cardiac/representation/reflection_spec.rb +73 -0
- data/spec/shared/cardiac/resource/adapter_spec.rb +83 -0
- data/spec/shared/cardiac/resource/builder_spec.rb +52 -0
- data/spec/shared/cardiac/resource/codec_methods_spec.rb +63 -0
- data/spec/shared/cardiac/resource/config_methods_spec.rb +52 -0
- data/spec/shared/cardiac/resource/extension_methods_spec.rb +215 -0
- data/spec/shared/cardiac/resource/request_methods_spec.rb +186 -0
- data/spec/shared/cardiac/resource/uri_methods_spec.rb +212 -0
- data/spec/shared/support/client_execution.rb +28 -0
- data/spec/spec_helper.rb +24 -0
- metadata +463 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Cardiac::Model callback methods.
|
5
|
+
# Most of this has been "borrowed" from ActiveRecord.
|
6
|
+
module Callbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
CALLBACKS = [
|
10
|
+
:after_initialize, :after_find, :before_validation, :after_validation,
|
11
|
+
:before_save, :around_save, :after_save, :before_create, :around_create,
|
12
|
+
:after_create, :before_update, :around_update, :after_update,
|
13
|
+
:before_destroy, :around_destroy, :after_destroy
|
14
|
+
]
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
include ActiveModel::Callbacks
|
18
|
+
end
|
19
|
+
|
20
|
+
included do
|
21
|
+
include ActiveModel::Validations::Callbacks
|
22
|
+
|
23
|
+
define_model_callbacks :initialize, :find, :only => :after
|
24
|
+
define_model_callbacks :save, :create, :update, :destroy
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy #:nodoc:
|
28
|
+
run_callbacks(:destroy) { super }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_or_update #:nodoc:
|
34
|
+
run_callbacks(:save) { super }
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_record #:nodoc:
|
38
|
+
run_callbacks(:create) { super }
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_record(*) #:nodoc:
|
42
|
+
run_callbacks(:update) { super }
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Cardiac::Model declaration methods and resource extensions.
|
5
|
+
module Declarations
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# This extension block is used to build the base resource's extension module.
|
9
|
+
RESOURCE_EXTENSION_BLOCK = Proc.new do
|
10
|
+
|
11
|
+
##
|
12
|
+
# :method: find_instances
|
13
|
+
# This member performs a GET, after merging any arguments into the query string.
|
14
|
+
# This member is used by find(:all) and find_all
|
15
|
+
operation :find_instances, lambda{|*query| query.any? ? query(*query).get : get }
|
16
|
+
|
17
|
+
##
|
18
|
+
# :method: create_instance
|
19
|
+
# This member performs a POST, using the given argument as the payload.
|
20
|
+
# This member is used by create_record.
|
21
|
+
operation :create_instance, lambda{|payload| post(payload) }
|
22
|
+
|
23
|
+
##
|
24
|
+
# :method: identify
|
25
|
+
# This member identifies a singular subresource by converting the given argument
|
26
|
+
# to a parameter and appending it to the path.
|
27
|
+
# This member is used by all query/persistence methods that operate on existing records.
|
28
|
+
subresource :identify, lambda{|id_or_model| path(id_or_model.to_param) } do
|
29
|
+
|
30
|
+
##
|
31
|
+
# :method: update_instance
|
32
|
+
# This member performs a PUT, using the given argument as the payload.
|
33
|
+
# This member is used by update_record.
|
34
|
+
operation :update_instance, lambda{|payload| put(payload) }
|
35
|
+
|
36
|
+
##
|
37
|
+
# :method: delete_instance
|
38
|
+
# This member performs a DELETE, after merging any arguments into the query string.
|
39
|
+
# This member is used by delete and destroy.
|
40
|
+
operation :delete_instance, lambda{|*query| query.any? ? query(*query).delete : delete }
|
41
|
+
|
42
|
+
##
|
43
|
+
# :method: find_instance
|
44
|
+
# This member performs a GET, after merging any arguments into the query string.
|
45
|
+
# This member is used by find(:one), find(:some), find_one, find_some, and find_with_ids
|
46
|
+
operation :find_instance, lambda{|*query| query.any? ? query(*query).get : get }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
include ::Cardiac::Declarations
|
51
|
+
|
52
|
+
included do
|
53
|
+
singleton_class.alias_method_chain :base_resource=, :extensions
|
54
|
+
singleton_class.alias_method_chain :resource, :extensions
|
55
|
+
end
|
56
|
+
|
57
|
+
# Implement an instance's base resource as a subresource of the class base resource.
|
58
|
+
# This prevents modifications to the subresource from persisting in the system.
|
59
|
+
#
|
60
|
+
# Persisted instances will use the +identify+ subresource, otherwise the base resource is used.
|
61
|
+
def base_resource
|
62
|
+
Subresource.new persisted? ? self.class.identify(self) : self.class.base_resource
|
63
|
+
end
|
64
|
+
|
65
|
+
module ClassMethods
|
66
|
+
|
67
|
+
# Overridden to extend the base resource with persistence operations.
|
68
|
+
def base_resource_with_extensions=(base)
|
69
|
+
case base
|
70
|
+
when ::URI, ::String, ::Cardiac::Resource
|
71
|
+
# Extend the resource with additional declarations before assigning it.
|
72
|
+
base = DeclarationBuilder.new(base).extension_eval(&RESOURCE_EXTENSION_BLOCK)
|
73
|
+
end
|
74
|
+
self.base_resource_without_extensions = base
|
75
|
+
end
|
76
|
+
|
77
|
+
# Overridden to ensure that the resource is first extended with persistence operations.
|
78
|
+
def resource_with_extensions base=nil, &declaration
|
79
|
+
self.base_resource = base if base.present?
|
80
|
+
resource_without_extensions base_resource, &declaration
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Internal method that checks for the presence of an extension method on the resource.
|
86
|
+
def resource_has_extension_method?(name,base=base_resource)
|
87
|
+
base && base.__extension_module__.method_defined?(name)
|
88
|
+
end
|
89
|
+
|
90
|
+
# FIXME: use a more optimized approach that falls back on method_missing.
|
91
|
+
def respond_to_missing?(name,include_private=false)
|
92
|
+
resource_has_extension_method?(name) || super
|
93
|
+
end
|
94
|
+
|
95
|
+
# FIXME: use a more optimized approach that falls back on method_missing.
|
96
|
+
def method_missing(name,*args,&block)
|
97
|
+
if resource_has_extension_method? name
|
98
|
+
perform_operation name, *args, &block
|
99
|
+
else
|
100
|
+
super
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
# Cardiac::Model dirty attribute methods.
|
4
|
+
# Some of this has been "borrowed" from ActiveRecord.
|
5
|
+
module Dirty
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include ActiveModel::Dirty
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :partial_updates
|
12
|
+
self.partial_updates = false # Off by default, unlike ActiveRecord.
|
13
|
+
end
|
14
|
+
|
15
|
+
# Attempts to +save+ the record and clears changed attributes if successful.
|
16
|
+
#
|
17
|
+
# @see ActiveRecord::Dirty#save
|
18
|
+
def save(*) #:nodoc:
|
19
|
+
if status = super
|
20
|
+
@previously_changed = changes
|
21
|
+
@changed_attributes.clear
|
22
|
+
#elsif IdentityMap.enabled?
|
23
|
+
# IdentityMap.remove(self)
|
24
|
+
end
|
25
|
+
status
|
26
|
+
end
|
27
|
+
|
28
|
+
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
29
|
+
#
|
30
|
+
# @see ActiveRecord::Dirty#save!
|
31
|
+
def save!(*) #:nodoc:
|
32
|
+
super.tap do
|
33
|
+
@previously_changed = changes
|
34
|
+
@changed_attributes.clear
|
35
|
+
end
|
36
|
+
#rescue
|
37
|
+
# IdentityMap.remove(self) if IdentityMap.enabled?
|
38
|
+
# raise
|
39
|
+
end
|
40
|
+
|
41
|
+
# <tt>reload</tt> the record and clears changed attributes.
|
42
|
+
#
|
43
|
+
# @see ActiveRecord::Dirty#reload
|
44
|
+
def reload(*) #:nodoc:
|
45
|
+
super.tap do
|
46
|
+
@previously_changed.clear
|
47
|
+
@changed_attributes.clear
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Wrap write_attribute to remember original attribute value.
|
54
|
+
#
|
55
|
+
# Very similar to ActiveRecord::Dirty, except that we are not dealing with timezones.
|
56
|
+
# The semantics of ActiveModel::Dirty#attribute_will_change! does not handle attributes
|
57
|
+
# changing _back_ to their original value. Thus, like ActiveRecord, we won't use it.
|
58
|
+
#
|
59
|
+
# @see ActiveRecord::Dirty#write_attribute
|
60
|
+
def attribute=(attr, value)
|
61
|
+
attr = attr.to_s
|
62
|
+
|
63
|
+
# The attribute already had an unsaved change, so check if it is changing back to the original.
|
64
|
+
if attribute_changed?(attr)
|
65
|
+
old = @changed_attributes[attr]
|
66
|
+
@changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
|
67
|
+
|
68
|
+
# No existing unsaved change, so simply remember this value if it differs from the original.
|
69
|
+
else
|
70
|
+
old = clone_attribute_value(:read_attribute, attr)
|
71
|
+
@changed_attributes[attr] = old if _field_changed?(attr, old, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
super(attr, value)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Wrap update_record to perform partial updates when configured to do so.
|
78
|
+
#
|
79
|
+
# @see ActiveRecord::Dirty#update
|
80
|
+
def update_record(*)
|
81
|
+
if partial_updates?
|
82
|
+
super(changed)
|
83
|
+
else
|
84
|
+
super
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Very similar to ActiveRecord::Dirty, except that we use ActiveAttr::Typecasting instead of columns.
|
89
|
+
# If no type is available, then just compare the values directly.
|
90
|
+
#
|
91
|
+
# @see ActiveRecord::Dirty#_field_changed?
|
92
|
+
def _field_changed?(attr, old, value)
|
93
|
+
if type = _attribute_type(attr)
|
94
|
+
if Numeric === type && (changes_from_nil_to_empty_string?(old, value) || changes_from_zero_to_string?(old, value))
|
95
|
+
value = nil
|
96
|
+
else
|
97
|
+
value = typecast_attribute(_attribute_typecaster(attr), value)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
old != value
|
101
|
+
end
|
102
|
+
|
103
|
+
# @see ActiveRecord::Dirty#changes_from_nil_to_empty_string?
|
104
|
+
def changes_from_nil_to_empty_string?(old, value)
|
105
|
+
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
106
|
+
# be typecast back to 0 (''.to_i => 0)
|
107
|
+
(old.nil? || old == 0) && value.blank?
|
108
|
+
end
|
109
|
+
|
110
|
+
# @see ActiveRecord::Dirty#changes_from_zero_to_string?
|
111
|
+
def changes_from_zero_to_string?(old, value)
|
112
|
+
# For fields with old 0 and value non-empty string
|
113
|
+
old == 0 && value.is_a?(String) && value.present? && value != '0'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
# Cardiac::Model operational methods.
|
4
|
+
module Operations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Extensions that are applied to the OperationHandler.
|
8
|
+
module HandlerExtensions
|
9
|
+
def transmit!(*args)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Extensions that are applied to the ResourceAdapter.
|
15
|
+
module AdapterExtensions
|
16
|
+
def __codecs__
|
17
|
+
@__codecs__ ||= Module.new{ include ::Cardiac::Representation::Codecs }
|
18
|
+
end
|
19
|
+
|
20
|
+
def __handler__
|
21
|
+
@__handler__ ||= Class.new(::Cardiac::OperationHandler){ include HandlerExtensions; self }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Extensions that are applied to the OperationProxy.
|
26
|
+
module ProxyExtensions
|
27
|
+
def __adapter__
|
28
|
+
@__adapter__ ||= Class.new(::Cardiac::ResourceAdapter){ include AdapterExtensions ; self }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# All remote operations go through this method.
|
37
|
+
def perform_operation(name, *args, &block)
|
38
|
+
proxy = __operation_proxy__.new(base_resource)
|
39
|
+
proxy.klass = self
|
40
|
+
proxy.__send__(name,*args,&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def __operation_proxy__
|
44
|
+
@__operation_proxy__ ||= Class.new(OperationProxy){ include ProxyExtensions ; self }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Cardiac::Model persistence methods.
|
5
|
+
# Most of this has been "borrowed" from ActiveRecord.
|
6
|
+
module Persistence
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# See ActiveRecord::Base.create
|
11
|
+
def create(attributes = nil, &block)
|
12
|
+
if attributes.is_a?(Array)
|
13
|
+
attributes.collect { |attr| create(attr, &block) }
|
14
|
+
else
|
15
|
+
object = new(attributes, &block)
|
16
|
+
object.save
|
17
|
+
object
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns true if this object hasn't been saved yet -- that is, a record
|
24
|
+
# for the object doesn't exist in the data store yet; otherwise, returns false.
|
25
|
+
def new_record?
|
26
|
+
@new_record
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns true if this object has been destroyed, otherwise returns false.
|
30
|
+
def destroyed?
|
31
|
+
@destroyed
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if the record is persisted, i.e. it's not a new record and it was
|
35
|
+
# not destroyed, otherwise returns false.
|
36
|
+
def persisted?
|
37
|
+
!(new_record? || destroyed?)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Performs a DELETE on the remote resource if the record is not new, marks it
|
41
|
+
# as destroyed, then freezes the attributes.
|
42
|
+
def delete
|
43
|
+
delete_record
|
44
|
+
freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
# Delegates to delete, except it raises a +ReadOnlyRecord+ error if the record is read-only.
|
48
|
+
def destroy
|
49
|
+
raise ReadOnlyRecord if readonly?
|
50
|
+
delete
|
51
|
+
end
|
52
|
+
|
53
|
+
# Delegates to destroy, but raises +RecordNotDestroyed+ if checks fail.
|
54
|
+
def destroy!
|
55
|
+
destroy || raise(RecordNotDestroyed)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Delegates to create_or_update, but rescues validation exceptions to return false.
|
59
|
+
def save(*)
|
60
|
+
create_or_update
|
61
|
+
rescue RecordInvalid
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
# Delegates to create_or_update, but raises +RecordNotSaved+ if validations fail.
|
66
|
+
def save!(*)
|
67
|
+
create_or_update || raise(RecordNotSaved)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Reloads the attributes of this object from the remote.
|
71
|
+
# Any (optional) arguments are passed to find when reloading.
|
72
|
+
def reload(*args)
|
73
|
+
#IdentityMap.without do
|
74
|
+
fresh_object = self.class.find(self.id, *args)
|
75
|
+
@attributes.update(fresh_object.instance_variable_get('@attributes'))
|
76
|
+
#end
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# Updates a single attribute and saves the record.
|
81
|
+
# This is especially useful for boolean flags on existing records. Also note that
|
82
|
+
#
|
83
|
+
# * Validation is skipped.
|
84
|
+
# * Callbacks are invoked.
|
85
|
+
# * updated_at/updated_on column is updated if that column is available.
|
86
|
+
# * Updates all the attributes that are dirty in this object.
|
87
|
+
#
|
88
|
+
# This method raises an +OperationFailError+ if the attribute is marked as readonly.
|
89
|
+
def update_attribute(name, value)
|
90
|
+
name = name.to_s
|
91
|
+
verify_readonly_attribute(name)
|
92
|
+
send("#{name}=", value)
|
93
|
+
save(validate: false)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Updates the attributes of the model from the passed-in hash and saves the
|
97
|
+
# record. If the object is invalid, the saving will fail and false will be returned.
|
98
|
+
def update(attributes)
|
99
|
+
assign_attributes(attributes)
|
100
|
+
save
|
101
|
+
end
|
102
|
+
|
103
|
+
alias update_attributes update
|
104
|
+
|
105
|
+
# Updates its receiver just like +update+ but calls <tt>save!</tt> instead
|
106
|
+
# of +save+, so an exception is raised if the record is invalid.
|
107
|
+
def update!(attributes)
|
108
|
+
assign_attributes(attributes)
|
109
|
+
save!
|
110
|
+
end
|
111
|
+
|
112
|
+
alias update_attributes! update!
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# Internal method that deletes an existing record, then marks this record as destroyed.
|
117
|
+
def delete_record
|
118
|
+
self.remote_attributes = self.class.identify(self).delete_instance if persisted?
|
119
|
+
@destroyed = true
|
120
|
+
end
|
121
|
+
|
122
|
+
# Internal method that either creates a new record, or updates an existing one.
|
123
|
+
# Raises a ReadOnlyRecord error if the record is read-only.
|
124
|
+
def create_or_update
|
125
|
+
raise ReadOnlyRecord if readonly?
|
126
|
+
result = new_record? ? create_record : update_record
|
127
|
+
result != false
|
128
|
+
end
|
129
|
+
|
130
|
+
# Internal method that updates an existing record.
|
131
|
+
def update_record(attribute_names = attributes.keys)
|
132
|
+
|
133
|
+
# Build a payload hash, but silently skip this operation if they are empty.
|
134
|
+
payload_hash = attributes_for_update(attribute_names)
|
135
|
+
return unless payload_hash.present?
|
136
|
+
|
137
|
+
# Perform the operation and save the attributes returned by the remote.
|
138
|
+
self.remote_attributes = self.class.identify(self).update_instance(payload_hash)
|
139
|
+
|
140
|
+
# Return success.
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
# Internal method that creates an existing record.
|
145
|
+
def create_record(attribute_names = @attributes.keys)
|
146
|
+
|
147
|
+
# Build a payload hash.
|
148
|
+
payload_hash = attributes_for_create(attribute_names)
|
149
|
+
|
150
|
+
# Perform the operation and save the attributes returned by the remote.
|
151
|
+
self.remote_attributes = self.class.create_instance(payload_hash)
|
152
|
+
|
153
|
+
# Write back any key attributes returned by the remote.
|
154
|
+
self.class.key_attributes.each do |key| key = key.to_s
|
155
|
+
write_attribute key, @remote_attributes[key] if @remote_attributes.has_key? key
|
156
|
+
end
|
157
|
+
|
158
|
+
# No longer a new record, if we at least have all key attributes defined.
|
159
|
+
@new_record = ! self.class.key_attributes.all?{|key| query_attribute key }
|
160
|
+
|
161
|
+
# Return success, if we are no longer a new record.
|
162
|
+
! @new_record
|
163
|
+
end
|
164
|
+
|
165
|
+
# @see ActiveRecord::Persistence#verify_readonly_attribute
|
166
|
+
def verify_readonly_attribute(name)
|
167
|
+
raise OperationFailError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Cardiac::Model finder methods.
|
5
|
+
# Some of this has been "borrowed" from ActiveRecord.
|
6
|
+
module Querying
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# This is a basic implementation that just delegates to find_all.
|
12
|
+
def all
|
13
|
+
find_all
|
14
|
+
end
|
15
|
+
|
16
|
+
# Simple pattern for delegating find operations to the resource.
|
17
|
+
# This is pretty similar to earlier AR versions that did not proxy to Relation.
|
18
|
+
#
|
19
|
+
# The biggest difference is that all finder methods accept an evaluator block
|
20
|
+
# allowing bulk operations to be performed on returned results.
|
21
|
+
def find(criteria=:all,*args,&evaluator)
|
22
|
+
case criteria
|
23
|
+
when :all, :first, :some, :one
|
24
|
+
send(:"find_#{criteria}", *args, &evaluator)
|
25
|
+
when Hash
|
26
|
+
find_all(*args.unshift(criteria), &evaluator)
|
27
|
+
when Array
|
28
|
+
find_with_ids(criteria, &evaluator)
|
29
|
+
when self, Numeric, String
|
30
|
+
find_one(criteria, &evaluator)
|
31
|
+
when Model::Base
|
32
|
+
find_one(criteria.id, &evaluator)
|
33
|
+
else
|
34
|
+
raise ArgumentError, "unsupported finder criteria: #{criteria}:#{criteria.class}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# Simple pattern for delegating find :first operations to the resource.
|
41
|
+
# Unlike the other finders, this one will return +nil+ instead of raising an error if it is not found.
|
42
|
+
def find_first(criteria,*args,&evaluator)
|
43
|
+
case criteria
|
44
|
+
when Array
|
45
|
+
criteria = criteria.first
|
46
|
+
when self, Numeric, String
|
47
|
+
# PASS-THROUGH
|
48
|
+
when Model::Base
|
49
|
+
criteria = criteria.id
|
50
|
+
else
|
51
|
+
raise ArgumentError, "unsupported find_first criteria: #{criteria}:#{criteria.class}"
|
52
|
+
end
|
53
|
+
find_by_identity(criteria,&evaluator)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Delegates to the find_instances operation on the resource.
|
57
|
+
def find_all(*args, &evaluator)
|
58
|
+
result = unwrap_remote_collection(find_instances(*args)) || []
|
59
|
+
raise InvalidRepresentationError, 'expected Array, but got '+result.class.name unless Array===result
|
60
|
+
result.map! do |record|
|
61
|
+
instantiate(record, remote: true, &evaluator)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# See ActiveRecord::Relation::FinderMethods#find_with_ids
|
66
|
+
def find_with_ids(*ids, &evaluator)
|
67
|
+
expects_array = ids.first.kind_of?(Array)
|
68
|
+
return ids.first if expects_array && ids.first.empty?
|
69
|
+
ids = ids.flatten.compact.uniq
|
70
|
+
case ids.size
|
71
|
+
when 0
|
72
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
73
|
+
when 1
|
74
|
+
result = find_one(ids.first, &evaluator)
|
75
|
+
expects_array ? [ result ] : result
|
76
|
+
else
|
77
|
+
find_some(ids, &evaluator)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# See ActiveRecord::Relation::FinderMethods#find_one
|
82
|
+
def find_one(id, &evaluator)
|
83
|
+
record = find_by_identity(id,&evaluator)
|
84
|
+
raise_record_not_found_exception!(id, 0, 1) unless record
|
85
|
+
record
|
86
|
+
end
|
87
|
+
|
88
|
+
# See ActiveRecord::Relation::FinderMethods#find_some
|
89
|
+
def find_some(ids, &evaluator)
|
90
|
+
results = ids.map{|id| find_by_identity(id,&evaluator) }
|
91
|
+
results.compact!
|
92
|
+
raise_record_not_found_exception!(ids, results.size, expected_size) unless results.size == ids.size
|
93
|
+
results
|
94
|
+
end
|
95
|
+
|
96
|
+
# @see ActiveRecord::Persistence#instantiate
|
97
|
+
def instantiate(record, options = {})
|
98
|
+
allocate.init_with options.merge('attributes'=>record).stringify_keys
|
99
|
+
end
|
100
|
+
|
101
|
+
# See ActiveRecord::Relation::FinderMethods#raise_record_not_found_exception
|
102
|
+
def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
|
103
|
+
if Array(ids).size == 1
|
104
|
+
error = "Couldn't find #{name} with #{key_attributes.first}=#{ids}"
|
105
|
+
else
|
106
|
+
error = "Couldn't find all #{name.pluralize} with IDs "
|
107
|
+
error << "(#{ids.join(", ")}) (found #{result_size} results, but was looking for #{expected_size})"
|
108
|
+
end
|
109
|
+
raise RecordNotFound, error
|
110
|
+
end
|
111
|
+
|
112
|
+
# Unwraps a collection payload returned by the remote.
|
113
|
+
def unwrap_remote_collection(data,options={})
|
114
|
+
unwrap_remote_data data, options
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Delegates to the resource to find a single instance of a record and return the remote payload.
|
120
|
+
def find_by_identity(id, &evaluator)
|
121
|
+
record = unwrap_remote_data identify(id).find_instance
|
122
|
+
instantiate(record, remote: true, &evaluator) if record
|
123
|
+
rescue Cardiac::RequestFailedError => e
|
124
|
+
raise e unless e.status == 404 # Ignores 404 Not Found
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|