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