cardiac 0.2.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/LICENSE +22 -0
  4. data/Rakefile +66 -0
  5. data/cardiac-0.2.0.pre2.gem +0 -0
  6. data/cardiac.gemspec +48 -0
  7. data/lib/cardiac/declarations.rb +70 -0
  8. data/lib/cardiac/errors.rb +65 -0
  9. data/lib/cardiac/log_subscriber.rb +55 -0
  10. data/lib/cardiac/model/attributes.rb +146 -0
  11. data/lib/cardiac/model/base.rb +161 -0
  12. data/lib/cardiac/model/callbacks.rb +47 -0
  13. data/lib/cardiac/model/declarations.rb +106 -0
  14. data/lib/cardiac/model/dirty.rb +117 -0
  15. data/lib/cardiac/model/locale/en.yml +7 -0
  16. data/lib/cardiac/model/operations.rb +49 -0
  17. data/lib/cardiac/model/persistence.rb +171 -0
  18. data/lib/cardiac/model/querying.rb +129 -0
  19. data/lib/cardiac/model/validations.rb +124 -0
  20. data/lib/cardiac/model.rb +17 -0
  21. data/lib/cardiac/operation_builder.rb +75 -0
  22. data/lib/cardiac/operation_handler.rb +215 -0
  23. data/lib/cardiac/railtie.rb +20 -0
  24. data/lib/cardiac/reflections.rb +85 -0
  25. data/lib/cardiac/representation.rb +124 -0
  26. data/lib/cardiac/resource/adapter.rb +178 -0
  27. data/lib/cardiac/resource/builder.rb +107 -0
  28. data/lib/cardiac/resource/codec_methods.rb +58 -0
  29. data/lib/cardiac/resource/config_methods.rb +39 -0
  30. data/lib/cardiac/resource/extension_methods.rb +115 -0
  31. data/lib/cardiac/resource/request_methods.rb +138 -0
  32. data/lib/cardiac/resource/subresource.rb +88 -0
  33. data/lib/cardiac/resource/uri_methods.rb +176 -0
  34. data/lib/cardiac/resource.rb +77 -0
  35. data/lib/cardiac/util.rb +120 -0
  36. data/lib/cardiac/version.rb +3 -0
  37. data/lib/cardiac.rb +61 -0
  38. data/spec/rails-3.2/Gemfile +9 -0
  39. data/spec/rails-3.2/Gemfile.lock +136 -0
  40. data/spec/rails-3.2/Rakefile +10 -0
  41. data/spec/rails-3.2/app_root/app/assets/javascripts/application.js +15 -0
  42. data/spec/rails-3.2/app_root/app/assets/stylesheets/application.css +13 -0
  43. data/spec/rails-3.2/app_root/app/controllers/application_controller.rb +3 -0
  44. data/spec/rails-3.2/app_root/app/helpers/application_helper.rb +2 -0
  45. data/spec/rails-3.2/app_root/app/views/layouts/application.html.erb +14 -0
  46. data/spec/rails-3.2/app_root/config/application.rb +29 -0
  47. data/spec/rails-3.2/app_root/config/boot.rb +13 -0
  48. data/spec/rails-3.2/app_root/config/database.yml +25 -0
  49. data/spec/rails-3.2/app_root/config/environment.rb +5 -0
  50. data/spec/rails-3.2/app_root/config/environments/development.rb +10 -0
  51. data/spec/rails-3.2/app_root/config/environments/production.rb +11 -0
  52. data/spec/rails-3.2/app_root/config/environments/test.rb +11 -0
  53. data/spec/rails-3.2/app_root/config/initializers/backtrace_silencers.rb +7 -0
  54. data/spec/rails-3.2/app_root/config/initializers/inflections.rb +15 -0
  55. data/spec/rails-3.2/app_root/config/initializers/mime_types.rb +5 -0
  56. data/spec/rails-3.2/app_root/config/initializers/secret_token.rb +7 -0
  57. data/spec/rails-3.2/app_root/config/initializers/session_store.rb +8 -0
  58. data/spec/rails-3.2/app_root/config/initializers/wrap_parameters.rb +14 -0
  59. data/spec/rails-3.2/app_root/config/locales/en.yml +5 -0
  60. data/spec/rails-3.2/app_root/config/routes.rb +2 -0
  61. data/spec/rails-3.2/app_root/db/test.sqlite3 +0 -0
  62. data/spec/rails-3.2/app_root/log/test.log +2403 -0
  63. data/spec/rails-3.2/app_root/public/404.html +26 -0
  64. data/spec/rails-3.2/app_root/public/422.html +26 -0
  65. data/spec/rails-3.2/app_root/public/500.html +25 -0
  66. data/spec/rails-3.2/app_root/public/favicon.ico +0 -0
  67. data/spec/rails-3.2/app_root/script/rails +6 -0
  68. data/spec/rails-3.2/spec/spec_helper.rb +25 -0
  69. data/spec/rails-4.0/Gemfile +9 -0
  70. data/spec/rails-4.0/Gemfile.lock +132 -0
  71. data/spec/rails-4.0/Rakefile +10 -0
  72. data/spec/rails-4.0/app_root/app/assets/javascripts/application.js +15 -0
  73. data/spec/rails-4.0/app_root/app/assets/stylesheets/application.css +13 -0
  74. data/spec/rails-4.0/app_root/app/controllers/application_controller.rb +3 -0
  75. data/spec/rails-4.0/app_root/app/helpers/application_helper.rb +2 -0
  76. data/spec/rails-4.0/app_root/app/views/layouts/application.html.erb +14 -0
  77. data/spec/rails-4.0/app_root/config/application.rb +28 -0
  78. data/spec/rails-4.0/app_root/config/boot.rb +13 -0
  79. data/spec/rails-4.0/app_root/config/database.yml +25 -0
  80. data/spec/rails-4.0/app_root/config/environment.rb +5 -0
  81. data/spec/rails-4.0/app_root/config/environments/development.rb +9 -0
  82. data/spec/rails-4.0/app_root/config/environments/production.rb +11 -0
  83. data/spec/rails-4.0/app_root/config/environments/test.rb +10 -0
  84. data/spec/rails-4.0/app_root/config/initializers/backtrace_silencers.rb +7 -0
  85. data/spec/rails-4.0/app_root/config/initializers/inflections.rb +15 -0
  86. data/spec/rails-4.0/app_root/config/initializers/mime_types.rb +5 -0
  87. data/spec/rails-4.0/app_root/config/initializers/secret_token.rb +7 -0
  88. data/spec/rails-4.0/app_root/config/initializers/session_store.rb +8 -0
  89. data/spec/rails-4.0/app_root/config/initializers/wrap_parameters.rb +14 -0
  90. data/spec/rails-4.0/app_root/config/locales/en.yml +5 -0
  91. data/spec/rails-4.0/app_root/config/routes.rb +2 -0
  92. data/spec/rails-4.0/app_root/db/test.sqlite3 +0 -0
  93. data/spec/rails-4.0/app_root/log/development.log +50 -0
  94. data/spec/rails-4.0/app_root/log/test.log +2399 -0
  95. data/spec/rails-4.0/app_root/public/404.html +26 -0
  96. data/spec/rails-4.0/app_root/public/422.html +26 -0
  97. data/spec/rails-4.0/app_root/public/500.html +25 -0
  98. data/spec/rails-4.0/app_root/public/favicon.ico +0 -0
  99. data/spec/rails-4.0/app_root/script/rails +6 -0
  100. data/spec/rails-4.0/spec/spec_helper.rb +25 -0
  101. data/spec/shared/cardiac/declarations_spec.rb +103 -0
  102. data/spec/shared/cardiac/model/base_spec.rb +446 -0
  103. data/spec/shared/cardiac/operation_builder_spec.rb +96 -0
  104. data/spec/shared/cardiac/operation_handler_spec.rb +82 -0
  105. data/spec/shared/cardiac/representation/reflection_spec.rb +73 -0
  106. data/spec/shared/cardiac/resource/adapter_spec.rb +83 -0
  107. data/spec/shared/cardiac/resource/builder_spec.rb +52 -0
  108. data/spec/shared/cardiac/resource/codec_methods_spec.rb +63 -0
  109. data/spec/shared/cardiac/resource/config_methods_spec.rb +52 -0
  110. data/spec/shared/cardiac/resource/extension_methods_spec.rb +215 -0
  111. data/spec/shared/cardiac/resource/request_methods_spec.rb +186 -0
  112. data/spec/shared/cardiac/resource/uri_methods_spec.rb +212 -0
  113. data/spec/shared/support/client_execution.rb +28 -0
  114. data/spec/spec_helper.rb +24 -0
  115. 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,7 @@
1
+ en:
2
+
3
+ # Cardiac models configuration
4
+ cardiac_model:
5
+ errors:
6
+ messages:
7
+ record_invalid: "Validation failed: %{errors}%{remote_errors}"
@@ -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