activegraph 11.0.0.beta.1-java

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 (144) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2016 -0
  3. data/CONTRIBUTORS +12 -0
  4. data/Gemfile +24 -0
  5. data/README.md +111 -0
  6. data/activegraph.gemspec +52 -0
  7. data/bin/rake +17 -0
  8. data/config/locales/en.yml +5 -0
  9. data/config/neo4j/add_classnames.yml +1 -0
  10. data/config/neo4j/config.yml +35 -0
  11. data/lib/active_graph.rb +123 -0
  12. data/lib/active_graph/ansi.rb +14 -0
  13. data/lib/active_graph/attribute_set.rb +32 -0
  14. data/lib/active_graph/base.rb +77 -0
  15. data/lib/active_graph/class_arguments.rb +39 -0
  16. data/lib/active_graph/config.rb +135 -0
  17. data/lib/active_graph/core.rb +14 -0
  18. data/lib/active_graph/core/connection_failed_error.rb +6 -0
  19. data/lib/active_graph/core/cypher_error.rb +37 -0
  20. data/lib/active_graph/core/entity.rb +11 -0
  21. data/lib/active_graph/core/instrumentable.rb +37 -0
  22. data/lib/active_graph/core/label.rb +135 -0
  23. data/lib/active_graph/core/logging.rb +44 -0
  24. data/lib/active_graph/core/node.rb +15 -0
  25. data/lib/active_graph/core/querable.rb +41 -0
  26. data/lib/active_graph/core/query.rb +485 -0
  27. data/lib/active_graph/core/query_builder.rb +18 -0
  28. data/lib/active_graph/core/query_clauses.rb +727 -0
  29. data/lib/active_graph/core/query_ext.rb +24 -0
  30. data/lib/active_graph/core/query_find_in_batches.rb +46 -0
  31. data/lib/active_graph/core/record.rb +51 -0
  32. data/lib/active_graph/core/result.rb +31 -0
  33. data/lib/active_graph/core/schema.rb +65 -0
  34. data/lib/active_graph/core/schema_errors.rb +12 -0
  35. data/lib/active_graph/core/wrappable.rb +30 -0
  36. data/lib/active_graph/errors.rb +59 -0
  37. data/lib/active_graph/lazy_attribute_hash.rb +38 -0
  38. data/lib/active_graph/migration.rb +148 -0
  39. data/lib/active_graph/migrations.rb +27 -0
  40. data/lib/active_graph/migrations/base.rb +77 -0
  41. data/lib/active_graph/migrations/check_pending.rb +20 -0
  42. data/lib/active_graph/migrations/helpers.rb +105 -0
  43. data/lib/active_graph/migrations/helpers/id_property.rb +72 -0
  44. data/lib/active_graph/migrations/helpers/relationships.rb +66 -0
  45. data/lib/active_graph/migrations/helpers/schema.rb +63 -0
  46. data/lib/active_graph/migrations/migration_file.rb +24 -0
  47. data/lib/active_graph/migrations/runner.rb +195 -0
  48. data/lib/active_graph/migrations/schema.rb +64 -0
  49. data/lib/active_graph/migrations/schema_migration.rb +14 -0
  50. data/lib/active_graph/model_schema.rb +139 -0
  51. data/lib/active_graph/node.rb +110 -0
  52. data/lib/active_graph/node/callbacks.rb +8 -0
  53. data/lib/active_graph/node/dependent.rb +11 -0
  54. data/lib/active_graph/node/dependent/association_methods.rb +49 -0
  55. data/lib/active_graph/node/dependent/query_proxy_methods.rb +52 -0
  56. data/lib/active_graph/node/dependent_callbacks.rb +31 -0
  57. data/lib/active_graph/node/enum.rb +26 -0
  58. data/lib/active_graph/node/has_n.rb +602 -0
  59. data/lib/active_graph/node/has_n/association.rb +278 -0
  60. data/lib/active_graph/node/has_n/association/rel_factory.rb +61 -0
  61. data/lib/active_graph/node/has_n/association/rel_wrapper.rb +23 -0
  62. data/lib/active_graph/node/has_n/association_cypher_methods.rb +108 -0
  63. data/lib/active_graph/node/id_property.rb +224 -0
  64. data/lib/active_graph/node/id_property/accessor.rb +62 -0
  65. data/lib/active_graph/node/initialize.rb +21 -0
  66. data/lib/active_graph/node/labels.rb +207 -0
  67. data/lib/active_graph/node/labels/index.rb +37 -0
  68. data/lib/active_graph/node/labels/reloading.rb +21 -0
  69. data/lib/active_graph/node/node_list_formatter.rb +13 -0
  70. data/lib/active_graph/node/node_wrapper.rb +54 -0
  71. data/lib/active_graph/node/orm_adapter.rb +82 -0
  72. data/lib/active_graph/node/persistence.rb +186 -0
  73. data/lib/active_graph/node/property.rb +60 -0
  74. data/lib/active_graph/node/query.rb +76 -0
  75. data/lib/active_graph/node/query/query_proxy.rb +367 -0
  76. data/lib/active_graph/node/query/query_proxy_eager_loading.rb +177 -0
  77. data/lib/active_graph/node/query/query_proxy_eager_loading/association_tree.rb +75 -0
  78. data/lib/active_graph/node/query/query_proxy_enumerable.rb +110 -0
  79. data/lib/active_graph/node/query/query_proxy_find_in_batches.rb +19 -0
  80. data/lib/active_graph/node/query/query_proxy_link.rb +139 -0
  81. data/lib/active_graph/node/query/query_proxy_methods.rb +303 -0
  82. data/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +99 -0
  83. data/lib/active_graph/node/query_methods.rb +68 -0
  84. data/lib/active_graph/node/reflection.rb +86 -0
  85. data/lib/active_graph/node/rels.rb +11 -0
  86. data/lib/active_graph/node/scope.rb +166 -0
  87. data/lib/active_graph/node/unpersisted.rb +48 -0
  88. data/lib/active_graph/node/validations.rb +59 -0
  89. data/lib/active_graph/paginated.rb +27 -0
  90. data/lib/active_graph/railtie.rb +108 -0
  91. data/lib/active_graph/relationship.rb +68 -0
  92. data/lib/active_graph/relationship/callbacks.rb +21 -0
  93. data/lib/active_graph/relationship/initialize.rb +28 -0
  94. data/lib/active_graph/relationship/persistence.rb +133 -0
  95. data/lib/active_graph/relationship/persistence/query_factory.rb +95 -0
  96. data/lib/active_graph/relationship/property.rb +92 -0
  97. data/lib/active_graph/relationship/query.rb +99 -0
  98. data/lib/active_graph/relationship/rel_wrapper.rb +31 -0
  99. data/lib/active_graph/relationship/related_node.rb +87 -0
  100. data/lib/active_graph/relationship/types.rb +80 -0
  101. data/lib/active_graph/relationship/validations.rb +8 -0
  102. data/lib/active_graph/schema/operation.rb +102 -0
  103. data/lib/active_graph/shared.rb +48 -0
  104. data/lib/active_graph/shared/attributes.rb +217 -0
  105. data/lib/active_graph/shared/callbacks.rb +66 -0
  106. data/lib/active_graph/shared/cypher.rb +37 -0
  107. data/lib/active_graph/shared/declared_properties.rb +204 -0
  108. data/lib/active_graph/shared/declared_property.rb +109 -0
  109. data/lib/active_graph/shared/declared_property/index.rb +37 -0
  110. data/lib/active_graph/shared/enum.rb +167 -0
  111. data/lib/active_graph/shared/filtered_hash.rb +79 -0
  112. data/lib/active_graph/shared/identity.rb +34 -0
  113. data/lib/active_graph/shared/initialize.rb +65 -0
  114. data/lib/active_graph/shared/marshal.rb +23 -0
  115. data/lib/active_graph/shared/mass_assignment.rb +63 -0
  116. data/lib/active_graph/shared/permitted_attributes.rb +28 -0
  117. data/lib/active_graph/shared/persistence.rb +272 -0
  118. data/lib/active_graph/shared/property.rb +249 -0
  119. data/lib/active_graph/shared/query_factory.rb +122 -0
  120. data/lib/active_graph/shared/rel_type_converters.rb +43 -0
  121. data/lib/active_graph/shared/serialized_properties.rb +30 -0
  122. data/lib/active_graph/shared/type_converters.rb +439 -0
  123. data/lib/active_graph/shared/typecasted_attributes.rb +99 -0
  124. data/lib/active_graph/shared/typecaster.rb +53 -0
  125. data/lib/active_graph/shared/validations.rb +44 -0
  126. data/lib/active_graph/tasks/migration.rake +204 -0
  127. data/lib/active_graph/timestamps.rb +11 -0
  128. data/lib/active_graph/timestamps/created.rb +9 -0
  129. data/lib/active_graph/timestamps/updated.rb +9 -0
  130. data/lib/active_graph/transaction.rb +22 -0
  131. data/lib/active_graph/transactions.rb +57 -0
  132. data/lib/active_graph/type_converters.rb +7 -0
  133. data/lib/active_graph/undeclared_properties.rb +53 -0
  134. data/lib/active_graph/version.rb +3 -0
  135. data/lib/active_graph/wrapper.rb +4 -0
  136. data/lib/rails/generators/active_graph/migration/migration_generator.rb +16 -0
  137. data/lib/rails/generators/active_graph/migration/templates/migration.erb +9 -0
  138. data/lib/rails/generators/active_graph/model/model_generator.rb +89 -0
  139. data/lib/rails/generators/active_graph/model/templates/migration.erb +11 -0
  140. data/lib/rails/generators/active_graph/model/templates/model.erb +15 -0
  141. data/lib/rails/generators/active_graph/upgrade_v8/templates/migration.erb +17 -0
  142. data/lib/rails/generators/active_graph/upgrade_v8/upgrade_v8_generator.rb +34 -0
  143. data/lib/rails/generators/active_graph_generator.rb +121 -0
  144. metadata +423 -0
@@ -0,0 +1,37 @@
1
+ module ActiveGraph::Node::Labels
2
+ module Index
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ extend Forwardable
7
+
8
+ def_delegators :declared_properties, :indexed_properties
9
+
10
+ # Creates a Neo4j index on given property
11
+ #
12
+ # This can also be done on the property directly, see ActiveGraph::Node::Property::ClassMethods#property.
13
+ #
14
+ # @param [Symbol] property the property we want a Neo4j index on
15
+ #
16
+ # @example
17
+ # class Person
18
+ # include ActiveGraph::Node
19
+ # property :name
20
+ # index :name
21
+ # end
22
+ def index(property)
23
+ return if ActiveGraph::ModelSchema.defined_constraint?(self, property)
24
+
25
+ ActiveGraph::ModelSchema.add_defined_index(self, property)
26
+ end
27
+
28
+ # Creates a neo4j constraint on this class for given property
29
+ #
30
+ # @example
31
+ # Person.constraint :name, type: :unique
32
+ def constraint(property, _constraints = {type: :unique})
33
+ ActiveGraph::ModelSchema.add_defined_constraint(self, property)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveGraph::Node::Labels
2
+ module Reloading
3
+ extend ActiveSupport::Concern
4
+
5
+ MODELS_TO_RELOAD = []
6
+
7
+ def self.reload_models!
8
+ MODELS_TO_RELOAD.each(&:constantize)
9
+ MODELS_TO_RELOAD.clear
10
+ end
11
+
12
+ module ClassMethods
13
+ def before_remove_const
14
+ associations.each_value(&:queue_model_refresh!)
15
+ MODELS_FOR_LABELS_CACHE.clear
16
+ WRAPPED_CLASSES.each { |c| MODELS_TO_RELOAD << c.name }
17
+ WRAPPED_CLASSES.clear
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveGraph::Node
2
+ class NodeListFormatter
3
+ def initialize(list, max_elements = 5)
4
+ @list = list
5
+ @max_elements = max_elements
6
+ end
7
+
8
+ def inspect
9
+ return @list.inspect if !@max_elements || @list.length <= @max_elements
10
+ "[#{@list.take(5).map!(&:inspect).join(', ')}, ...]"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/inflector'
2
+ require 'active_graph/core/node'
3
+
4
+ wrapping_proc = proc do |node|
5
+ found_class = ActiveGraph::NodeWrapping.class_to_wrap(node.labels)
6
+ next node if not found_class
7
+
8
+ found_class.new.tap do |wrapped_node|
9
+ wrapped_node.init_on_load(node, node.properties)
10
+ end
11
+ end
12
+ Neo4j::Driver::Types::Node.wrapper_callback(wrapping_proc)
13
+
14
+ module ActiveGraph
15
+ module NodeWrapping
16
+ # Only load classes once for performance
17
+ CONSTANTS_FOR_LABELS_CACHE = {}
18
+
19
+ class << self
20
+ def class_to_wrap(labels)
21
+ load_classes_from_labels(labels)
22
+ ActiveGraph::Node::Labels.model_for_labels(labels).tap do |model_class|
23
+ populate_constants_for_labels_cache(model_class, labels)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def load_classes_from_labels(labels)
30
+ labels.each { |label| constant_for_label(label) }
31
+ end
32
+
33
+ def constant_for_label(label)
34
+ CONSTANTS_FOR_LABELS_CACHE[label] || CONSTANTS_FOR_LABELS_CACHE[label] = constantized_label(label)
35
+ end
36
+
37
+ def constantized_label(label)
38
+ "#{association_model_namespace}::#{label}".constantize
39
+ rescue NameError, LoadError
40
+ nil
41
+ end
42
+
43
+ def populate_constants_for_labels_cache(model_class, labels)
44
+ labels.each do |label|
45
+ CONSTANTS_FOR_LABELS_CACHE[label] = model_class if CONSTANTS_FOR_LABELS_CACHE[label].nil?
46
+ end
47
+ end
48
+
49
+ def association_model_namespace
50
+ ActiveGraph::Config.association_model_namespace_string
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ require 'orm_adapter'
2
+
3
+ module ActiveGraph
4
+ module Node
5
+ module ClassMethods
6
+ include OrmAdapter::ToAdapter
7
+ end
8
+
9
+ class OrmAdapter < ::OrmAdapter::Base
10
+ module ClassMethods
11
+ include ActiveModel::Callbacks
12
+ end
13
+
14
+ def column_names
15
+ klass._decl_props.keys
16
+ end
17
+
18
+ def i18n_scope
19
+ :neo4j
20
+ end
21
+
22
+ # Get an instance by id of the model
23
+ def get!(id)
24
+ klass.find(wrap_key(id)).tap do |node|
25
+ fail 'No record found' if node.nil?
26
+ end
27
+ end
28
+
29
+ # Get an instance by id of the model
30
+ def get(id)
31
+ klass.find_by(klass.id_property_name => wrap_key(id))
32
+ end
33
+
34
+ # Find the first instance matching conditions
35
+ def find_first(options = {})
36
+ conditions, order = extract_conditions!(options)
37
+ extract_id!(conditions)
38
+ order = hasherize_order(order)
39
+
40
+ result = klass.where(conditions)
41
+ result = result.order(order) unless order.empty?
42
+ result.first
43
+ end
44
+
45
+ # Find all models matching conditions
46
+ def find_all(options = {})
47
+ conditions, order, limit, offset = extract_conditions!(options)
48
+ extract_id!(conditions)
49
+ order = hasherize_order(order)
50
+
51
+ result = klass.where(conditions)
52
+ result = result.order(order) unless order.empty?
53
+ result = result.skip(offset) if offset
54
+ result = result.limit(limit) if limit
55
+ result.to_a
56
+ end
57
+
58
+ # Create a model using attributes
59
+ def create!(attributes = {})
60
+ klass.create!(attributes)
61
+ end
62
+
63
+ # @see OrmAdapter::Base#destroy
64
+ def destroy(object)
65
+ object.destroy && true if valid_object?(object)
66
+ end
67
+
68
+ private
69
+
70
+ def hasherize_order(order)
71
+ (order || []).map { |clause| Hash[*clause] }
72
+ end
73
+
74
+ def extract_id!(conditions)
75
+ id = conditions.delete(:id)
76
+ return if not id
77
+
78
+ conditions[klass.id_property_name.to_sym] = id
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,186 @@
1
+ module ActiveGraph::Node
2
+ module Persistence
3
+ class RecordInvalidError < RuntimeError
4
+ attr_reader :record
5
+
6
+ def initialize(record)
7
+ @record = record
8
+ super(@record.errors.full_messages.join(', '))
9
+ end
10
+ end
11
+
12
+ extend ActiveSupport::Concern
13
+ extend Forwardable
14
+ include ActiveGraph::Shared::Persistence
15
+
16
+ # Saves the model.
17
+ #
18
+ # If the model is new a record gets created in the database, otherwise the existing record gets updated.
19
+ # If perform_validation is true validations run.
20
+ # If any of them fail the action is cancelled and save returns false.
21
+ # If the flag is false validations are bypassed altogether.
22
+ # See ActiveRecord::Validations for more information.
23
+ # There's a series of callbacks associated with save.
24
+ # If any of the before_* callbacks return false the action is cancelled and save returns false.
25
+ def save(*)
26
+ cascade_save do
27
+ association_proxy_cache.clear
28
+ create_or_update
29
+ end
30
+ end
31
+
32
+ # Increments concurrently a numeric attribute by a centain amount
33
+ # @param [Symbol, String] attribute name of the attribute to increment
34
+ # @param [Integer, Float] by amount to increment
35
+ def concurrent_increment!(attribute, by = 1)
36
+ increment_by_query! query_as(:n), attribute, by
37
+ end
38
+
39
+ # Persist the object to the database. Validations and Callbacks are included
40
+ # by default but validation can be disabled by passing :validate => false
41
+ # to #save! Creates a new transaction.
42
+ #
43
+ # @raise a RecordInvalidError if there is a problem during save.
44
+ # @param (see ActiveGraph::Rails::Validations#save)
45
+ # @return nil
46
+ # @see #save
47
+ # @see ActiveGraph::Rails::Validations ActiveGraph::Rails::Validations - for the :validate parameter
48
+ # @see ActiveGraph::Rails::Callbacks ActiveGraph::Rails::Callbacks - for callbacks
49
+ def save!(*args)
50
+ save(*args) or fail(RecordInvalidError, self) # rubocop:disable Style/AndOr
51
+ end
52
+
53
+ # Creates a model with values matching those of the instance attributes and returns its id.
54
+ # @private
55
+ # @return true
56
+ def create_model
57
+ node = _create_node(props_for_create)
58
+ init_on_load(node, node.properties)
59
+ @deferred_nodes = nil
60
+ true
61
+ end
62
+
63
+ # TODO: This does not seem like it should be the responsibility of the node.
64
+ # Creates an unwrapped node in the database.
65
+ # @param [Hash] node_props The type-converted properties to be added to the new node.
66
+ # @param [Array] labels The labels to use for creating the new node.
67
+ # @return [ActiveGraph::Node] A CypherNode or EmbeddedNode
68
+ def _create_node(node_props, labels = labels_for_create)
69
+ query = "CREATE (n:`#{Array(labels).join('`:`')}`) SET n = $props RETURN n"
70
+ neo4j_query(query, {props: node_props}, wrap: false).to_a[0][:n]
71
+ end
72
+
73
+ # As the name suggests, this inserts the primary key (id property) into the properties hash.
74
+ # The method called here, `default_property_values`, is a holdover from an earlier version of the gem. It does NOT
75
+ # contain the default values of properties, it contains the Default Property, which we now refer to as the ID Property.
76
+ # It will be deprecated and renamed in a coming refactor.
77
+ # @param [Hash] converted_props A hash of properties post-typeconversion, ready for insertion into the DB.
78
+ def inject_primary_key!(converted_props)
79
+ self.class.default_property_values(self).tap do |destination_props|
80
+ destination_props.merge!(converted_props) if converted_props.is_a?(Hash)
81
+ end
82
+ end
83
+
84
+ # @return [Array] Labels to be set on the node during a create event
85
+ def labels_for_create
86
+ self.class.mapped_label_names
87
+ end
88
+
89
+ private
90
+
91
+ def destroy_query
92
+ query_as(:n).break.optional_match('(n)-[r]-()').delete(:n, :r)
93
+ end
94
+
95
+ # The pending associations are cleared during the save process, so it's necessary to
96
+ # build the processable hash before it begins.
97
+ def cascade_save
98
+ ActiveGraph::Base.transaction do
99
+ yield.tap { process_unpersisted_nodes! }
100
+ end
101
+ end
102
+
103
+ module ClassMethods
104
+ # Creates and saves a new node
105
+ # @param [Hash] props the properties the new node should have
106
+ def create(props = {})
107
+ new(props).tap do |obj|
108
+ yield obj if block_given?
109
+ obj.save
110
+ end
111
+ end
112
+
113
+ # Same as #create, but raises an error if there is a problem during save.
114
+ def create!(props = {})
115
+ new(props).tap do |o|
116
+ yield o if block_given?
117
+ o.save!
118
+ end
119
+ end
120
+
121
+ def merge(match_attributes, optional_attrs = {})
122
+ options = [:on_create, :on_match, :set]
123
+ optional_attrs.assert_valid_keys(*options)
124
+
125
+ optional_attrs.default = {}
126
+ on_create_attrs, on_match_attrs, set_attrs = optional_attrs.values_at(*options)
127
+
128
+ new_query.merge(n: {self.mapped_label_names => match_attributes})
129
+ .on_create_set(on_create_clause(on_create_attrs))
130
+ .on_match_set(on_match_clause(on_match_attrs))
131
+ .break.set(n: set_attrs)
132
+ .pluck(:n).first
133
+ end
134
+
135
+ def find_or_create(find_attributes, set_attributes = {})
136
+ on_create_attributes = set_attributes.reverse_merge(find_attributes.merge(self.new(find_attributes).props_for_create))
137
+
138
+ new_query.merge(n: {self.mapped_label_names => find_attributes})
139
+ .on_create_set(n: on_create_attributes)
140
+ .pluck(:n).first
141
+ end
142
+
143
+ # Finds the first node with the given attributes, or calls create if none found
144
+ def find_or_create_by(attributes, &block)
145
+ find_by(attributes) || create(attributes, &block)
146
+ end
147
+
148
+ # Same as #find_or_create_by, but calls #create! so it raises an error if there is a problem during save.
149
+ def find_or_create_by!(attributes, &block)
150
+ find_by(attributes) || create!(attributes, &block)
151
+ end
152
+
153
+ def find_or_initialize_by(attributes)
154
+ find_by(attributes) || new(attributes).tap { |o| yield(o) if block_given? }
155
+ end
156
+
157
+ def load_entity(id)
158
+ query = query_base_for(id, :n).return(:n)
159
+ result = neo4j_query(query).first
160
+ result && result[:n]
161
+ end
162
+
163
+ def query_base_for(neo_id, var = :n)
164
+ ActiveGraph::Base.new_query.match(var).where(var => {neo_id: neo_id})
165
+ end
166
+
167
+ private
168
+
169
+ def on_create_clause(clause)
170
+ if clause.is_a?(Hash)
171
+ {n: clause.merge(self.new(clause).props_for_create)}
172
+ else
173
+ clause
174
+ end
175
+ end
176
+
177
+ def on_match_clause(clause)
178
+ if clause.is_a?(Hash)
179
+ {n: clause.merge(attributes_nil_hash.key?('updated_at') ? {updated_at: Time.new.to_i} : {})}
180
+ else
181
+ clause
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveGraph::Node
2
+ module Property
3
+ extend ActiveSupport::Concern
4
+ include ActiveGraph::Shared::Property
5
+
6
+ def initialize(attributes = nil)
7
+ super(attributes)
8
+ @attributes ||= ActiveGraph::AttributeSet.new(self.class.attributes_nil_hash, self.class.attributes.keys)
9
+ end
10
+
11
+ module ClassMethods
12
+ # Extracts keys from attributes hash which are associations of the model
13
+ # TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
14
+ def extract_association_attributes!(attributes)
15
+ return unless contains_association?(attributes)
16
+ attributes.each_with_object({}) do |(key, _), result|
17
+ result[key] = attributes.delete(key) if self.association_key?(key)
18
+ end
19
+ end
20
+
21
+ def association_key?(key)
22
+ association_method_keys.include?(key.to_sym)
23
+ end
24
+
25
+ private
26
+
27
+ def contains_association?(attributes)
28
+ return false unless attributes
29
+ attributes.each_key { |k| return true if association_key?(k) }
30
+ false
31
+ end
32
+
33
+ # All keys which could be association setter methods (including _id/_ids)
34
+ def association_method_keys
35
+ @association_method_keys ||=
36
+ associations_keys.map(&:to_sym) +
37
+ associations.values.map do |association|
38
+ if association.type == :has_one
39
+ "#{association.name}_id"
40
+ elsif association.type == :has_many
41
+ "#{association.name.to_s.singularize}_ids"
42
+ end.to_sym
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def inspect_attributes
50
+ id_property_name = self.class.id_property_name.to_s
51
+
52
+ attribute_pairs = attributes.except(id_property_name).sort.map do |key, value|
53
+ [key, (value.is_a?(String) && value.size > 100) ? value.dup[0..100] : value]
54
+ end
55
+
56
+ attribute_pairs.unshift([id_property_name, self.send(id_property_name)])
57
+ attribute_pairs
58
+ end
59
+ end
60
+ end