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.
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