cardiac 0.2.0.pre2
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.
- 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
|