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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ffcee4b9c9da2df77d5d3530d5562848b497fdab
4
+ data.tar.gz: d0dc3f4297ef67ddc38f139be6b64b00b7ff4dc9
5
+ SHA512:
6
+ metadata.gz: d01919339fec4e2519bc8b92479383d7c259247fcae5055e1a2722baceca9f91e070398660c04103a6fb0ffdb70c55faf23a9d04973e0faf24f8fdbf45cc9715
7
+ data.tar.gz: 55c38c5f11b07b8d92fc6d314d6c3a674bf8bfed80a936ab54b755042d185745d6fa76d217551d851772a00a418b664f24f24293f8df95fc0cb048df1b77f767
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Joe Khoobyar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/Rakefile ADDED
@@ -0,0 +1,66 @@
1
+ require 'rake'
2
+ require 'bundler/gem_tasks'
3
+
4
+ desc 'Default: Run all specs.'
5
+ task :default => 'all:spec'
6
+
7
+ namespace :all do
8
+
9
+ desc "Run specs on all spec apps"
10
+ task :spec do
11
+ success = true
12
+ for_each_directory_of('spec/**/Rakefile') do |directory|
13
+ env = "SPEC=../../#{ENV['SPEC']} " if ENV['SPEC']
14
+ success &= system("cd #{directory} && #{env} bundle exec rake spec")
15
+ end
16
+ fail "Tests failed" unless success
17
+ end
18
+
19
+ namespace :bundle do
20
+
21
+ desc "Bundle all spec apps"
22
+ task :install do
23
+ for_each_directory_of('spec/**/Gemfile') do |directory|
24
+ Bundler.with_clean_env do
25
+ system("cd #{directory} && bundle install")
26
+ end
27
+ end
28
+ end
29
+
30
+ desc "Update all gems, or a list of gem given by the GEM environment variable"
31
+ task :update do
32
+ for_each_directory_of('spec/**/Gemfile') do |directory|
33
+ Bundler.with_clean_env do
34
+ system("cd #{directory} && bundle update #{ENV['GEM']}")
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ #desc "Bundle the gem"
44
+ #task :bundle => [:bundle_install] do
45
+ # sh 'gem build *.gemspec'
46
+ # sh 'gem install *.gem'
47
+ # sh 'rm *.gem'
48
+ #end
49
+ #
50
+ #desc "Runs bundle install"
51
+ #task :bundle_install do
52
+ # sh('bundle install')
53
+ #end
54
+
55
+ desc "generate rdoc"
56
+ task :rdoc do
57
+ sh "yardoc"
58
+ end
59
+
60
+ def for_each_directory_of(path, &block)
61
+ Dir[path].sort.each do |rakefile|
62
+ directory = File.dirname(rakefile)
63
+ puts '', "\033[44m#{directory}\033[0m", ''
64
+ block.call(directory)
65
+ end
66
+ end
Binary file
data/cardiac.gemspec ADDED
@@ -0,0 +1,48 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cardiac/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cardiac"
7
+ s.version = Cardiac::VERSION
8
+ s.authors = ["Joe Khoobyar"]
9
+ s.email = ["joe@khoobyar.name"]
10
+ s.homepage = "http://github.com/cardiac/cardiac"
11
+ s.license = 'MIT'
12
+ s.summary = %q{ Cardiac: a REST modeling framework for Ruby }
13
+ s.description = %q{
14
+ This gem provides a thin facade around REST-ful resources, aiming to be closer to ActiveRecord than ActiveResource.
15
+ }
16
+
17
+ s.rubyforge_project = "cardiac"
18
+
19
+ s.required_ruby_version = '>= 1.9.2'
20
+
21
+ s.files = (Dir.glob("*") + Dir.glob("lib/**/*")).delete_if do |item|
22
+ item.include?("rdoc") ||
23
+ item.include?(".git")
24
+ end
25
+ s.require_paths = ["lib"]
26
+
27
+ s.test_files = (['.rspec'] + Dir.glob("spec/**/*")).delete_if do |item|
28
+ item.include?("rdoc") ||
29
+ item.include?(".git")
30
+ end
31
+
32
+ s.add_development_dependency 'rake', '~> 10.1'
33
+ s.add_development_dependency 'rspec', '~> 3.0', '< 3.1'
34
+ s.add_development_dependency 'rspec-rails', '~> 3.0', '< 3.1'
35
+ s.add_development_dependency 'rspec-collection_matchers', '~> 1.0'
36
+
37
+ s.add_runtime_dependency "rack", '>= 1.4.5'
38
+ s.add_runtime_dependency "rack-cache", '~> 1.2'
39
+ s.add_runtime_dependency "rack-client", '~> 0.4.2'
40
+ s.add_runtime_dependency "activesupport", '>= 3.2', '< 4.1'
41
+ s.add_runtime_dependency "multi_json", '~> 1.0'
42
+ s.add_runtime_dependency "json", '> 1.8.0'
43
+ s.add_runtime_dependency "mime-types", '> 1.1'
44
+ s.add_runtime_dependency "i18n", '~> 0.6', '>= 0.6.4'
45
+ s.add_runtime_dependency "activemodel", '>= 3.2', '< 4.1'
46
+ s.add_runtime_dependency "active_attr", '>= 0.8.2'
47
+
48
+ end
@@ -0,0 +1,70 @@
1
+ module Cardiac
2
+ class DeclarationBuilder < ::Cardiac::ExtensionBuilder
3
+ # Overridden to build directly on a resource/subresource.
4
+ def initialize(base, extension_module=nil)
5
+ case base
6
+ when ::Cardiac::Resource
7
+ builder = ::Cardiac::Subresource.new(base)
8
+ when ::URI, ::String
9
+ builder = ::Cardiac::Resource.new(base)
10
+ else
11
+ raise ArgumentError, 'a base URI or Resource must be provided'
12
+ end
13
+ super builder, extension_module
14
+ end
15
+
16
+ # Overridden to return an extended resource/subresource,
17
+ # but skip building the extension module if no block is given.
18
+ def extension_exec(*args, &block)
19
+ builder = super(*args, &block)
20
+ builder.extending(__extension_module__) if block
21
+ builder
22
+ end
23
+ end
24
+
25
+ module DeclarationMethods
26
+
27
+ # Declares a new resource off of the given base, using the given extension block.
28
+ def resource base, &declaration
29
+ DeclarationBuilder.new(base).extension_eval(&declaration)
30
+ end
31
+
32
+ # Dynamically declares a new subresource (optionally targetting a given sub-url),
33
+ # and yields a new OperationProxy which targets that subresource.
34
+ def with_resource(at=nil)
35
+ yield OperationProxy.new(at ? Subresource.new(base_resource).at(at) : base_resource)
36
+ end
37
+ end
38
+
39
+ module Declarations
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ if respond_to? :class_attribute
44
+ class_attribute :base_resource, instance_reader: false, instance_writer: false
45
+ else
46
+ mattr_accessor :base_resource, instance_reader: false, instance_writer: false
47
+ end
48
+ end
49
+
50
+ # Extensions for a class or even a module, so it may declare resources.
51
+ module ClassMethods
52
+ include DeclarationMethods
53
+
54
+ delegate :base_url, to: :base_resource
55
+
56
+ # Overridden to always write to the base_resource
57
+ def resource base=base_resource, &declaration
58
+ self.base_resource = super(base, &declaration)
59
+ end
60
+ end
61
+
62
+ # Instance-level extensions for declaring resources.
63
+ include DeclarationMethods
64
+
65
+ # These start out as private, especially since an instance-level :with_resource
66
+ # would need an instance-level implementation of :base_resource
67
+ private :resource, :with_resource
68
+ end
69
+
70
+ end
@@ -0,0 +1,65 @@
1
+ module Cardiac
2
+
3
+ # Exception classes.
4
+ class ProtocolError < StandardError
5
+ end
6
+ class ResourceError < StandardError
7
+ end
8
+ class UnresolvableResourceError < ResourceError
9
+ end
10
+ class InvalidOperationError < ResourceError
11
+ end
12
+ class InvalidRepresentationError < ResourceError
13
+ end
14
+ class OperationAbortError < ResourceError
15
+ end
16
+ class OperationFailError < ResourceError
17
+ end
18
+
19
+ # Thrown when a request has failed, due to a non-2xx status code.
20
+ class RequestFailedError < ResourceError
21
+ attr_reader :response
22
+ def initialize(response,message=nil)
23
+ @response = response
24
+ super(message || Rack::Utils::HTTP_STATUS_CODES[@response.status])
25
+ end
26
+ end
27
+
28
+ # @see ActiveRecord::RecordInvalid
29
+ class RecordInvalid < ResourceError
30
+ attr_reader :record # :nodoc:
31
+ def initialize(record) # :nodoc:
32
+ @record = record
33
+
34
+ errors = @record.errors.full_messages.join(", ")
35
+ remote_errors = @record.remote_errors.full_messages.join(", ")
36
+
37
+ if remote_errors.present?
38
+ remote_errors = "(previous remote operation) #{remote_errors}"
39
+ errors += ' ' if errors.present?
40
+ end
41
+
42
+ super I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid",
43
+ errors: errors,
44
+ remote_errors: remote_errors,
45
+ default: :"errors.messages.record_invalid")
46
+ end
47
+ end
48
+
49
+ # @see ActiveRecord::RecordNotFound
50
+ class RecordNotFound < OperationFailError
51
+ end
52
+
53
+ # @see ActiveRecord::RecordNotSaved
54
+ class RecordNotSaved < OperationFailError
55
+ end
56
+
57
+ # @see ActiveRecord::RecordNotDestroyed
58
+ class RecordNotDestroyed < OperationFailError
59
+ end
60
+
61
+ # @see ActiveRecord::ReadOnlyRecord
62
+ class ReadOnlyRecord < OperationFailError
63
+ end
64
+
65
+ end
@@ -0,0 +1,55 @@
1
+ module Cardiac
2
+
3
+ class LogSubscriber < ActiveSupport::LogSubscriber
4
+
5
+ delegate :logger, to: '::Cardiac::Model::Base'
6
+
7
+ def initialize
8
+ super
9
+ @odd_or_even = false
10
+ end
11
+
12
+ def operation(event)
13
+ return unless logger.debug?
14
+
15
+ payload = event.payload
16
+
17
+ url = payload[:url]
18
+ stats = "#{event.duration.round(1)}ms"
19
+ stats = "CACHED #{stats}" if /fresh/ === payload[:response_headers].try(:[],'X-Rack-Client-Cache')
20
+ name = "#{payload[:name]} #{payload[:verb]} (#{stats})"
21
+
22
+ if extra = payload.except(:name, :verb, :url, :response_headers).presence
23
+ extra = " " + extra.map{|key,value|
24
+ key = key.to_s.underscore.upcase
25
+ "#{key}: #{key=='PAYLOAD' ? value : value.inspect}"
26
+ }.join(",\n\t +")
27
+ end
28
+
29
+ if odd?
30
+ name = color(name, CYAN, true)
31
+ url = color(url, nil, true)
32
+ else
33
+ name = color(name, MAGENTA, true)
34
+ end
35
+
36
+ debug " #{name} #{url}#{extra}"
37
+ end
38
+
39
+ def identity(event)
40
+ return unless logger.debug?
41
+
42
+ name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
43
+ line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
44
+
45
+ debug " #{name} #{line}"
46
+ end
47
+
48
+ def odd?
49
+ @odd_or_even = !@odd_or_even
50
+ end
51
+ end
52
+
53
+ # Make sure that the log subscriber is listening for events.
54
+ LogSubscriber.attach_to :cardiac
55
+ end
@@ -0,0 +1,146 @@
1
+ module Cardiac
2
+ module Model
3
+
4
+ # Cardiac::Model attribute methods.
5
+ # Some of this has been "borrowed" from ActiveRecord.
6
+ module Attributes
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ # Overridden to support passing in aliases at the same time.
12
+ # This could decrease the code size for attributes declarations by almost 50% for models with
13
+ # non-friendly remote attribute names.
14
+ def attribute(name,options={})
15
+ aliases = Array(options[:aliases])
16
+ super name, options.except(:aliases)
17
+ aliases.each{|k| alias_attribute k, name } if aliases
18
+ end
19
+
20
+ protected
21
+
22
+ # Unwraps a payload returned by the remote, requiring it to be present.
23
+ #
24
+ # Callers could pass <code>{allow_empty: true}</code> in the options to prevent
25
+ # empty results from being converted to <code>nil</code>.
26
+ def unwrap_remote_data(data,options={})
27
+ data = data.values.first if Hash===data && data.keys.size==1 && data.keys.first.to_s!='errors'
28
+ data = data.presence unless options[:allow_empty]
29
+ data
30
+ end
31
+ end
32
+
33
+ included do
34
+ class_attribute :readonly_attributes, instance_writer: false
35
+ class_attribute :key_attributes, instance_writer: false
36
+ class_attribute :id_delimiter
37
+
38
+ self.readonly_attributes = []
39
+ self.key_attributes = [:id]
40
+ self.id_delimiter = '-'
41
+ end
42
+
43
+ # Retrieves the most recently unpacked/decoded remote attributes.
44
+ def remote_attributes
45
+ @remote_attributes.with_indifferent_access
46
+ end
47
+
48
+ # Overridden to use this model's key_attributes to build the key.
49
+ # Returns an array of the key values, if they are all present, otherwise, returns nil.
50
+ def to_key
51
+ keys = key_attributes.presence and keys.map do |key|
52
+ return unless query_attribute(key)
53
+ read_attribute(key)
54
+ end
55
+ end
56
+
57
+ # This baseline implementation uses this model's id_delimiter to join what is returned by to_key.
58
+ # If the id_delimiter is set to nil, it will simply return an array instead.
59
+ #
60
+ # NOTE: Defining an :id attribute on your model will override this implementation.
61
+ def id
62
+ delim, values = id_delimiter, to_key
63
+ (delim && values) ? values.join(delim) : values
64
+ end
65
+
66
+ protected
67
+
68
+ # Stores the attributes returned by the remote, after performing any unpacking/decoding.
69
+ def assign_remote_attributes(data,options={})
70
+ @remote_attributes = Hash[
71
+ decode_remote_attributes(unwrap_remote_attributes(data, options), options).map do |key,value|
72
+ [key, value.duplicable? ? value.clone : value]
73
+ end
74
+ ]
75
+ end
76
+
77
+ alias remote_attributes= assign_remote_attributes
78
+
79
+ # Returns a copy of this model's attributes, cloning the duplicable values.
80
+ def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
81
+ attribute_names.each do |name|
82
+ attributes[name] = clone_attribute_value(reader_method, name)
83
+ end
84
+ attributes
85
+ end
86
+
87
+ # Reads an attribute value and returns a clone, if it is duplicable, or the original value, if not.
88
+ def clone_attribute_value(reader_method, attribute_name) # :nodoc:
89
+ value = send(reader_method, attribute_name)
90
+ value.duplicable? ? value.clone : value
91
+ rescue TypeError, NoMethodError
92
+ value
93
+ end
94
+
95
+ # Unwraps attributes returned by the remote, requiring the data to be non-empty.
96
+ #
97
+ # Callers could pass <code>{allow_empty: true}</code> in the options to prevent
98
+ # empty results from being converted to <code>nil</code>.
99
+ #
100
+ # NOTE: The baseline implementation just delegates to the class method: unwrap_remote_data
101
+ def unwrap_remote_attributes(data,options={})
102
+ self.class.send :unwrap_remote_data, data, options
103
+ end
104
+
105
+ # Decodes attributes returned by the remote, requiring the data to be non-nil.
106
+ # Callers could pass <code>{only: ...}</code> or <code>{except: ...}</code> in the options
107
+ # to filter the attributes by key.
108
+ #
109
+ # If the remote did not return a Hash, the data is first wrapped in a single key: <code>:data</code>
110
+ def decode_remote_attributes(data,options={})
111
+ unless data.nil?
112
+ data = Hash===data ? data.with_indifferent_access : {data: data}
113
+ data = data.slice(*options[:only]) if options[:only]
114
+ data = data.except(*options[:except]) if options[:except]
115
+ end
116
+ data
117
+ end
118
+
119
+ private
120
+
121
+ # Filters the primary keys and readonly attributes from the attribute names.
122
+ def attributes_for_update(attribute_names)
123
+ attributes.slice(*attribute_names).except(*(readonly_attributes+key_attributes))
124
+ end
125
+
126
+ # Filters out the primary keys, from the attribute names, when the primary
127
+ # key is to be generated (e.g. the id attribute has no value).
128
+ def attributes_for_create(attribute_names)
129
+ attributes.slice(*attribute_names).except(*key_attributes.reject{|k| query_attribute(k) })
130
+ end
131
+
132
+ def readonly_attribute?(name)
133
+ self.class.readonly_attributes.include?(name)
134
+ end
135
+
136
+ def key_attribute?(name)
137
+ self.class.key_attributes.include?(name)
138
+ end
139
+
140
+ # No seralized attribute support yet.
141
+ def serialized_attribute_value(name)
142
+ read_attribute(name)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,161 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_attr'
3
+
4
+ module Cardiac
5
+ module Model
6
+ class Base
7
+ include ActiveAttr::Model
8
+ include Cardiac::Model::Attributes
9
+ include Cardiac::Model::Querying
10
+ include Cardiac::Model::Persistence
11
+ include Cardiac::Model::Validations
12
+ include Cardiac::Model::Dirty
13
+ include Cardiac::Model::Callbacks
14
+ include Cardiac::Model::Declarations
15
+ include Cardiac::Model::Operations
16
+ include ActiveSupport::Configurable
17
+
18
+ # Instances may not explicitly define their own base resource.
19
+ undef_method :resource if method_defined? :resource
20
+
21
+ # Expose instance-level with_resource since there is a useful instance-level implementation of base_resource.
22
+ public :with_resource
23
+
24
+ config_accessor :operation_context
25
+ self.operation_context = {}.with_indifferent_access
26
+
27
+ # @see ActiveRecord::Core
28
+ mattr_accessor :logger, instance_writer: false
29
+
30
+ # Configure whether or not to treat all instances as read-only.
31
+ class_attribute :readonly, instance_writer: false
32
+
33
+ # Subclasses have an internationalization scope separate from active model.
34
+ def self.i18n_scope
35
+ :cardiac_model
36
+ end
37
+
38
+ # @see ActiveRecord::Core#initialize
39
+ def initialize(attributes = nil, options = {})
40
+ super
41
+ init_internals
42
+ init_changed_attributes
43
+ run_callbacks :initialize unless _initialize_callbacks.empty?
44
+ end
45
+
46
+ # @see ActiveRecord::Core#init_with
47
+ def init_with(coder)
48
+ _remote = coder['remote']
49
+ self.attributes = _remote ? {} : coder['attributes']
50
+ init_internals
51
+ @new_record = false
52
+ if _remote
53
+ _remote = {} unless Hash===_remote
54
+ self.attributes = decode_remote_attributes( unwrap_remote_attributes(coder['attributes'], _remote),
55
+ _remote )
56
+ @changed_attributes.clear
57
+ end
58
+ run_callbacks :find
59
+ run_callbacks :initialize
60
+ self
61
+ end
62
+
63
+ # @see ActiveRecord::Core#slice
64
+ def slice(*methods)
65
+ Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
66
+ end
67
+
68
+ # @see ActiveRecord::Core#inspect
69
+ def inspect
70
+ defined?(@attributes) && @attributes ? super : "#<#{self.class} not initialized>"
71
+ end
72
+
73
+ # @see ActiveRecord::Core#encode_with
74
+ def encode_with(coder)
75
+ coder['attributes'] = attributes
76
+ end
77
+
78
+ # @see ActiveRecord::Core#==
79
+ def ==(comparison_object)
80
+ super ||
81
+ comparison_object.instance_of?(self.class) &&
82
+ id.present? &&
83
+ comparison_object.id == id
84
+ end
85
+ alias :eql? :==
86
+
87
+ # @see ActiveRecord::Core#hash
88
+ def hash
89
+ id.hash
90
+ end
91
+
92
+ # @see ActiveRecord::Core#freeze
93
+ def freeze
94
+ @attributes = @attributes.clone.freeze
95
+ self
96
+ end
97
+
98
+ # @see ActiveRecord::Core#frozen
99
+ def frozen?
100
+ @attributes.frozen?
101
+ end
102
+
103
+ # @see ActiveRecord::Core#<=>
104
+ def <=>(other_object)
105
+ if other_object.is_a?(self.class)
106
+ self.to_key <=> other_object.to_key
107
+ end
108
+ end
109
+
110
+ # @see ActiveRecord::Core#readonly?
111
+ def readonly?
112
+ @readonly
113
+ end
114
+
115
+ # @see ActiveRecord::Core#readonly!
116
+ def readonly!
117
+ @readonly = true
118
+ end
119
+
120
+ private
121
+
122
+ # @see ActiveRecord::Core#init_changed_attributes
123
+ def init_changed_attributes
124
+ attribute_defaults.each do |name,value|
125
+ @changed_attributes[name] = value if _field_changed?(name, value, @attributes[name])
126
+ end
127
+ end
128
+
129
+ # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
130
+ # of the array, and then rescues from the possible NoMethodError. If those elements are
131
+ # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
132
+ # which significantly impacts upon performance.
133
+ #
134
+ # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
135
+ #
136
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
137
+ def to_ary # :nodoc:
138
+ nil
139
+ end
140
+
141
+ # @see ActiveRecord::Core#init_internals
142
+ def init_internals
143
+ @attributes ||= {}
144
+
145
+ self.class.key_attributes.each do |key|
146
+ @attributes[key] = nil unless @attributes.key?(key)
147
+ end
148
+
149
+ @previously_changed = {}
150
+ @changed_attributes = {}
151
+ @readonly = !! self.class.readonly?
152
+ @destroyed = false
153
+ @new_record = true
154
+ @remote_attributes = {}
155
+ end
156
+ end
157
+
158
+ ActiveSupport.run_load_hooks(:cardiac, Base)
159
+ end
160
+
161
+ end