activegraph 11.0.0.beta.1-java

Sign up to get free protection for your applications and to get access to all the features.
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,249 @@
1
+ module ActiveGraph::Shared
2
+ module Property
3
+ extend ActiveSupport::Concern
4
+
5
+ include ActiveGraph::Shared::MassAssignment
6
+ include ActiveGraph::Shared::TypecastedAttributes
7
+ include ActiveModel::Dirty
8
+
9
+ class UndefinedPropertyError < ActiveGraph::Error; end
10
+ class MultiparameterAssignmentError < ActiveGraph::Error; end
11
+
12
+ attr_reader :_persisted_obj
13
+
14
+ # This list should not be statically created. All types which have converters should by type casted
15
+ NEO4J_DRIVER_DATA_TYPES = [Hash, Neo4j::Driver::Types::Bytes, ActiveSupport::Duration, Neo4j::Driver::Types::Point,
16
+ Neo4j::Driver::Types::OffsetTime, Neo4j::Driver::Types::LocalTime, Neo4j::Driver::Types::LocalDateTime]
17
+
18
+ # TODO: Set @attribute correctly using class ActiveModel::Attribute, and after that
19
+ # remove mutations_from_database and other ActiveModel::Dirty overrided methods
20
+ def mutations_from_database
21
+ @mutations_from_database ||=
22
+ defined?(ActiveModel::ForcedMutationTracker) ? ActiveModel::ForcedMutationTracker.new(self) : ActiveModel::NullMutationTracker.instance
23
+ end
24
+
25
+ def inspect
26
+ attribute_descriptions = inspect_attributes.map do |key, value|
27
+ "#{ActiveGraph::ANSI::CYAN}#{key}: #{ActiveGraph::ANSI::CLEAR}#{value.inspect}"
28
+ end.join(', ')
29
+
30
+ separator = ' ' unless attribute_descriptions.empty?
31
+ "#<#{ActiveGraph::ANSI::YELLOW}#{self.class.name}#{ActiveGraph::ANSI::CLEAR}#{separator}#{attribute_descriptions}>"
32
+ end
33
+
34
+ def initialize(attributes = nil)
35
+ @attributes ||= ActiveGraph::AttributeSet.new({}, self.class.attributes.keys)
36
+ attributes = process_attributes(attributes)
37
+ modded_attributes = inject_defaults!(attributes)
38
+ validate_attributes!(modded_attributes)
39
+ writer_method_props = extract_writer_methods!(modded_attributes)
40
+ send_props(writer_method_props)
41
+ self.undeclared_properties = attributes
42
+ @_persisted_obj = nil
43
+ end
44
+
45
+ def undeclared_properties=(_); end
46
+
47
+ def inject_defaults!(starting_props)
48
+ return starting_props if self.class.declared_properties.declared_property_defaults.empty?
49
+ self.class.declared_properties.inject_defaults!(self, starting_props || {})
50
+ end
51
+
52
+ def read_attribute(name)
53
+ respond_to?(name) ? send(name) : nil
54
+ end
55
+ alias [] read_attribute
56
+
57
+ def send_props(hash)
58
+ return hash if hash.blank?
59
+ hash.each { |key, value| send("#{key}=", value) }
60
+ end
61
+
62
+ def reload_properties!(properties)
63
+ @attributes = nil
64
+ convert_and_assign_attributes(properties)
65
+ end
66
+
67
+ private
68
+
69
+ # Changes attributes hash to remove relationship keys
70
+ # Raises an error if there are any keys left which haven't been defined as properties on the model
71
+ # TODO: use declared_properties instead of self.attributes
72
+ def validate_attributes!(attributes)
73
+ return attributes if attributes.blank?
74
+ invalid_properties = attributes.keys.map(&:to_s) - self.attributes.keys
75
+ invalid_properties.reject! { |name| self.respond_to?("#{name}=") }
76
+ fail UndefinedPropertyError, "Undefined properties: #{invalid_properties.join(',')}" if !invalid_properties.empty?
77
+ end
78
+
79
+ def extract_writer_methods!(attributes)
80
+ return attributes if attributes.blank?
81
+ {}.tap do |writer_method_props|
82
+ attributes.keys.each do |key|
83
+ writer_method_props[key] = attributes.delete(key) if self.respond_to?("#{key}=")
84
+ end
85
+ end
86
+ end
87
+
88
+ DATE_KEY_REGEX = /\A([^\(]+)\((\d+)([ifs])\)$/
89
+ # Gives support for Rails date_select, datetime_select, time_select helpers.
90
+ def process_attributes(attributes = nil)
91
+ return attributes if attributes.blank?
92
+ multi_parameter_attributes = {}
93
+ new_attributes = {}
94
+ attributes.each_pair do |key, value|
95
+ if key.match(DATE_KEY_REGEX)
96
+ match = key.to_s.match(DATE_KEY_REGEX)
97
+ found_key = match[1]
98
+ index = match[2].to_i
99
+ (multi_parameter_attributes[found_key] ||= {})[index] = value.empty? ? nil : value.send("to_#{$3}")
100
+ else
101
+ new_attributes[key] = value
102
+ end
103
+ end
104
+
105
+ multi_parameter_attributes.empty? ? new_attributes : process_multiparameter_attributes(multi_parameter_attributes, new_attributes)
106
+ end
107
+
108
+ def process_multiparameter_attributes(multi_parameter_attributes, new_attributes)
109
+ multi_parameter_attributes.each_with_object(new_attributes) do |(key, values), attributes|
110
+ values = (values.keys.min..values.keys.max).map { |i| values[i] }
111
+ if (field = self.class.attributes[key.to_sym]).nil?
112
+ fail MultiparameterAssignmentError, "error on assignment #{values.inspect} to #{key}"
113
+ end
114
+
115
+ attributes[key] = instantiate_object(field, values)
116
+ end
117
+ end
118
+
119
+ def instantiate_object(field, values_with_empty_parameters)
120
+ return nil if values_with_empty_parameters.all?(&:nil?)
121
+ values = values_with_empty_parameters.collect { |v| v.nil? ? 1 : v }
122
+ klass = field.type
123
+ klass ? klass.new(*values) : values
124
+ end
125
+
126
+ module ClassMethods
127
+ extend Forwardable
128
+
129
+ def_delegators :declared_properties, :serialized_properties, :serialized_properties=, :serialize, :declared_property_defaults
130
+
131
+ VALID_PROPERTY_OPTIONS = %w(type default index constraint serializer typecaster).map(&:to_sym)
132
+ # Defines a property on the class
133
+ #
134
+ # See active_attr gem for allowed options, e.g which type
135
+ # Notice, in ActiveGraph you don't have to declare properties before using them, see the ActiveGraph::Coree api.
136
+ #
137
+ # @example Without type
138
+ # class Person
139
+ # # declare a property which can have any value
140
+ # property :name
141
+ # end
142
+ #
143
+ # @example With type and a default value
144
+ # class Person
145
+ # # declare a property which can have any value
146
+ # property :score, type: Integer, default: 0
147
+ # end
148
+ #
149
+ # @example With an index
150
+ # class Person
151
+ # # declare a property which can have any value
152
+ # property :name, index: :exact
153
+ # end
154
+ #
155
+ # @example With a constraint
156
+ # class Person
157
+ # # declare a property which can have any value
158
+ # property :name, constraint: :unique
159
+ # end
160
+ def property(name, options = {})
161
+ invalid_option_keys = options.keys.map(&:to_sym) - VALID_PROPERTY_OPTIONS
162
+ fail ArgumentError, "Invalid options for property `#{name}` on `#{self.name}`: #{invalid_option_keys.join(', ')}" if invalid_option_keys.any?
163
+ build_property(name, options) do |prop|
164
+ attribute(prop)
165
+ end
166
+ end
167
+
168
+ # @param [Symbol] name The property name
169
+ # @param [ActiveGraph::Shared::AttributeDefinition] attr_def A cloned AttributeDefinition to reuse
170
+ # @param [Hash] options An options hash to use in the new property definition
171
+ def inherit_property(name, attr_def, options = {})
172
+ build_property(name, options) do |prop_name|
173
+ attributes[prop_name] = attr_def
174
+ end
175
+ end
176
+
177
+ def build_property(name, options)
178
+ decl_prop = DeclaredProperty.new(name, options).tap do |prop|
179
+ prop.register
180
+ declared_properties.register(prop)
181
+ yield name
182
+ constraint_or_index(name, options)
183
+ end
184
+
185
+ # If this class has already been inherited, make sure subclasses inherit property
186
+ subclasses.each do |klass|
187
+ klass.inherit_property name, decl_prop.clone, declared_properties[name].options
188
+ end
189
+
190
+ decl_prop
191
+ end
192
+
193
+ def undef_property(name)
194
+ undef_constraint_or_index(name)
195
+ declared_properties.unregister(name)
196
+ attribute_methods(name).each { |method| undef_method(method) }
197
+ end
198
+
199
+ def declared_properties
200
+ @_declared_properties ||= DeclaredProperties.new(self)
201
+ end
202
+
203
+ # @return [Hash] A frozen hash of all model properties with nil values. It is used during node loading and prevents
204
+ # an extra call to a slow dependency method.
205
+ def attributes_nil_hash
206
+ declared_properties.attributes_nil_hash
207
+ end
208
+
209
+ def extract_association_attributes!(props)
210
+ props
211
+ end
212
+
213
+ private
214
+
215
+ def attribute!(name)
216
+ remove_instance_variable('@attribute_methods_generated') if instance_variable_defined?('@attribute_methods_generated')
217
+ define_attribute_methods([name]) unless attribute_names.include?(name)
218
+ attributes[name.to_s] = declared_properties[name]
219
+ define_method("#{name}=") do |value|
220
+ typecast_value = if NEO4J_DRIVER_DATA_TYPES.include?(_attribute_type(name))
221
+ value
222
+ else
223
+ typecast_attribute(_attribute_typecaster(name), value)
224
+ end
225
+ send("#{name}_will_change!") unless typecast_value == read_attribute(name)
226
+ super(value)
227
+ end
228
+ end
229
+
230
+ def constraint_or_index(name, options)
231
+ # either constraint or index, do not set both
232
+ if options[:constraint]
233
+ fail "unknown constraint type #{options[:constraint]}, only :unique supported" if options[:constraint] != :unique
234
+ constraint(name, type: :unique)
235
+ elsif options[:index]
236
+ fail "unknown index type #{options[:index]}, only :exact supported" if options[:index] != :exact
237
+ index(name) if options[:index] == :exact
238
+ end
239
+ end
240
+
241
+ def undef_constraint_or_index(name)
242
+ prop = declared_properties[name]
243
+ return unless prop.index_or_constraint?
244
+ type = prop.constraint? ? :constraint : :index
245
+ send(:"drop_#{type}", name)
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,122 @@
1
+ module ActiveGraph::Shared
2
+ # Acts as a bridge between the node and rel models and ActiveGraph::Core::Query.
3
+ # If the object is persisted, it returns a query matching; otherwise, it returns a query creating it.
4
+ # This class does not execute queries, so it keeps no record of what identifiers have been set or what has happened in previous factories.
5
+ class QueryFactory
6
+ attr_reader :graph_object, :identifier
7
+
8
+ def initialize(graph_object, identifier)
9
+ @graph_object = graph_object
10
+ @identifier = identifier.to_sym
11
+ end
12
+
13
+ def self.create(graph_object, identifier)
14
+ factory_for(graph_object).new(graph_object, identifier)
15
+ end
16
+
17
+ def self.factory_for(graph_obj)
18
+ case
19
+ when graph_obj.respond_to?(:labels_for_create)
20
+ NodeQueryFactory
21
+ when graph_obj.respond_to?(:type)
22
+ RelQueryFactory
23
+ else
24
+ fail "Unable to find factory for #{graph_obj}"
25
+ end
26
+ end
27
+
28
+ def query
29
+ graph_object.persisted? ? match_query : create_query
30
+ end
31
+
32
+ # @param [ActiveGraph::Core::Query] query An instance of ActiveGraph::Core::Query upon which methods will be chained.
33
+ def base_query=(query)
34
+ return if query.blank?
35
+ @base_query = query
36
+ end
37
+
38
+ def base_query
39
+ @base_query || ActiveGraph::Base.new_query
40
+ end
41
+
42
+ protected
43
+
44
+ def create_query
45
+ fail 'Abstract class, not implemented'
46
+ end
47
+
48
+ def match_query
49
+ base_query
50
+ .match(match_string).where("ID(#{identifier}) = $#{identifier_id}")
51
+ .params(identifier_id.to_sym => graph_object.neo_id)
52
+ end
53
+
54
+ def identifier_id
55
+ @identifier_id ||= "#{identifier}_id"
56
+ end
57
+
58
+ def identifier_params
59
+ @identifier_params ||= "#{identifier}_params"
60
+ end
61
+ end
62
+
63
+ class NodeQueryFactory < QueryFactory
64
+ protected
65
+
66
+ def match_string
67
+ "(#{identifier})"
68
+ end
69
+
70
+ def create_query
71
+ return match_query if graph_object.persisted?
72
+ labels = graph_object.labels_for_create.map { |l| ":`#{l}`" }.join
73
+ base_query.create("(#{identifier}#{labels} $#{identifier}_params)").params(identifier_params => graph_object.props_for_create)
74
+ end
75
+ end
76
+
77
+ class RelQueryFactory < QueryFactory
78
+ protected
79
+
80
+ def match_string
81
+ "(#{graph_object.from_node_identifier})-[#{identifier}]->()"
82
+ end
83
+
84
+ def create_query
85
+ return match_query if graph_object.persisted?
86
+ create_props, set_props = filtered_props
87
+ base_query.send(graph_object.create_method, query_string(create_props)).break
88
+ .set(identifier => set_props)
89
+ .params(params(create_props))
90
+ end
91
+
92
+ private
93
+
94
+ def filtered_props
95
+ ActiveGraph::Shared::FilteredHash.new(graph_object.props_for_create, graph_object.creates_unique_option).filtered_base
96
+ end
97
+
98
+ def query_string(create_props)
99
+ "(#{graph_object.from_node_identifier})-[#{identifier}:`#{graph_object.type}` #{pattern(create_props)}]->(#{graph_object.to_node_identifier})"
100
+ end
101
+
102
+ def params(create_props)
103
+ unique? ? create_props.transform_keys { |key| scoped(key).to_sym } : { namespace.to_sym => create_props }
104
+ end
105
+
106
+ def unique?
107
+ graph_object.create_method == :create_unique
108
+ end
109
+
110
+ def pattern(create_props)
111
+ unique? ? "{#{create_props.keys.map { |key| "#{key}: $#{scoped(key)}" }.join(', ')}}" : "$#{namespace}"
112
+ end
113
+
114
+ def scoped(key)
115
+ "#{namespace}_#{key}"
116
+ end
117
+
118
+ def namespace
119
+ "#{identifier}_create_props"
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveGraph::Shared
2
+ # This module controls changes to relationship type based on ActiveGraph::Config.transform_rel_type.
3
+ # It's used whenever a rel type is automatically determined based on Relationship model name or
4
+ # association type.
5
+ module RelTypeConverters
6
+ def decorated_rel_type(type)
7
+ @decorated_rel_type ||= ActiveGraph::Shared::RelTypeConverters.decorated_rel_type(type)
8
+ end
9
+
10
+ class << self
11
+ # Determines how relationship types should look when inferred based on association or Relationship model name.
12
+ # With the exception of `:none`, all options will call `underscore`, so `ThisClass` becomes `this_class`, with capitalization
13
+ # determined by the specific option passed.
14
+ # Valid options:
15
+ # * :upcase - `:this_class`, `ThisClass`, `thiS_claSs` (if you don't like yourself) becomes `THIS_CLASS`
16
+ # * :downcase - same as above, only... downcased.
17
+ # * :legacy - downcases and prepends `#`, so ThisClass becomes `#this_class`
18
+ # * :none - uses the string version of whatever is passed with no modifications
19
+ def rel_transformer
20
+ @rel_transformer ||= ActiveGraph::Config[:transform_rel_type].nil? ? :upcase : ActiveGraph::Config[:transform_rel_type]
21
+ end
22
+
23
+ # @param [String,Symbol] type The raw string or symbol to be used as the basis of the relationship type
24
+ # @return [String] A string that conforms to the set rel type conversion setting.
25
+ def decorated_rel_type(type)
26
+ type = type.to_s
27
+ decorated_type = case rel_transformer
28
+ when :upcase
29
+ type.underscore.upcase
30
+ when :downcase
31
+ type.underscore.downcase
32
+ when :legacy
33
+ "##{type.underscore.downcase}"
34
+ when :none
35
+ type
36
+ else
37
+ type.underscore.upcase
38
+ end
39
+ decorated_type.tap { |s| s.gsub!('/', '::') if type.include?('::') }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveGraph::Shared
2
+ # This module adds the `serialize` class method. It lets you store hashes and arrays in Neo4j properties.
3
+ # Be aware that you won't be able to search within serialized properties and stuff use indexes. If you do a regex search for portion of a string
4
+ # property, the search happens in Cypher and you may take a performance hit.
5
+ #
6
+ # See type_converters.rb for the serialization process.
7
+ module SerializedProperties
8
+ extend ActiveSupport::Concern
9
+
10
+ def serialized_properties
11
+ self.class.serialized_properties
12
+ end
13
+
14
+ def serializable_hash(*args)
15
+ super.merge(id: id)
16
+ end
17
+
18
+
19
+ module ClassMethods
20
+ def inherited(other)
21
+ inherit_serialized_properties(other) if self.respond_to?(:serialized_properties)
22
+ super
23
+ end
24
+
25
+ def inherit_serialized_properties(other)
26
+ other.serialized_properties = self.serialized_properties
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,439 @@
1
+ require 'date'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+ require 'active_support/core_ext/big_decimal/conversions'
5
+ require 'active_support/core_ext/string/conversions'
6
+
7
+ module ActiveGraph::Shared
8
+ class Boolean; end
9
+
10
+ module TypeConverters
11
+ CONVERTERS = {}
12
+
13
+ class Boolean; end
14
+
15
+ class BaseConverter
16
+ class << self
17
+ def converted?(value)
18
+ value.is_a?(db_type)
19
+ end
20
+ end
21
+
22
+ def supports_array?
23
+ false
24
+ end
25
+ end
26
+
27
+ class IntegerConverter < BaseConverter
28
+ NEO4J_LARGEST_INT = 9223372036854775807
29
+ NEO4J_SMALLEST_INT = -9223372036854775808
30
+ class << self
31
+ def converted?(value)
32
+ false
33
+ end
34
+
35
+ def convert_type
36
+ Integer
37
+ end
38
+
39
+ def db_type
40
+ Integer
41
+ end
42
+
43
+ def to_db(value)
44
+ val = value.to_i
45
+ val > NEO4J_LARGEST_INT || val < NEO4J_SMALLEST_INT ? val.to_s : val
46
+ end
47
+
48
+ def to_ruby(value)
49
+ value.to_i
50
+ end
51
+ end
52
+ end
53
+
54
+ class FloatConverter < BaseConverter
55
+ class << self
56
+ def convert_type
57
+ Float
58
+ end
59
+
60
+ def db_type
61
+ Float
62
+ end
63
+
64
+ def to_db(value)
65
+ value.to_f
66
+ end
67
+
68
+ alias to_ruby to_db
69
+ end
70
+ end
71
+
72
+ class BigDecimalConverter < BaseConverter
73
+ class << self
74
+ def convert_type
75
+ BigDecimal
76
+ end
77
+
78
+ def db_type
79
+ String
80
+ end
81
+
82
+ def to_db(value)
83
+ case value
84
+ when Rational
85
+ value.to_f.to_d
86
+ when respond_to?(:to_d)
87
+ value.to_d
88
+ else
89
+ BigDecimal(value.to_s)
90
+ end.to_s
91
+ end
92
+
93
+ def to_ruby(value)
94
+ value.to_d
95
+ end
96
+ end
97
+ end
98
+
99
+ class StringConverter < BaseConverter
100
+ class << self
101
+ def convert_type
102
+ String
103
+ end
104
+
105
+ def db_type
106
+ String
107
+ end
108
+
109
+ def to_db(value)
110
+ value.to_s
111
+ end
112
+
113
+ alias to_ruby to_db
114
+ end
115
+ end
116
+
117
+ # Converts Java long types to Date objects. Must be timezone UTC.
118
+ class DateConverter < BaseConverter
119
+ class << self
120
+ def convert_type
121
+ Date
122
+ end
123
+
124
+ def db_type
125
+ Date
126
+ end
127
+
128
+ def to_ruby(value)
129
+ value.respond_to?(:to_date) ? value.to_date : Time.at(value).utc.to_date
130
+ end
131
+
132
+ alias to_db to_ruby
133
+ end
134
+ end
135
+
136
+ # Converts DateTime objects to and from Java long types. Must be timezone UTC.
137
+ class DateTimeConverter < BaseConverter
138
+ class << self
139
+ def convert_type
140
+ DateTime
141
+ end
142
+
143
+ def db_type
144
+ Integer
145
+ end
146
+
147
+ # Converts the given DateTime (UTC) value to an Integer.
148
+ # DateTime values are automatically converted to UTC.
149
+ def to_db(value)
150
+ value = value.new_offset(0) if value.respond_to?(:new_offset)
151
+
152
+ args = [value.year, value.month, value.day]
153
+ args += (value.class == Date ? [0, 0, 0] : [value.hour, value.min, value.sec])
154
+
155
+ Time.utc(*args).to_i
156
+ end
157
+
158
+ def to_ruby(value)
159
+ return value if value.is_a?(DateTime)
160
+ t = case value
161
+ when Time
162
+ return value.to_datetime.utc
163
+ when Integer
164
+ Time.at(value).utc
165
+ when String
166
+ return value.to_datetime
167
+ else
168
+ fail ArgumentError, "Invalid value type for DateType property: #{value.inspect}"
169
+ end
170
+
171
+ DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
172
+ end
173
+ end
174
+ end
175
+
176
+ class TimeConverter < BaseConverter
177
+ class << self
178
+ def convert_type
179
+ Time
180
+ end
181
+
182
+ def db_type
183
+ Time
184
+ end
185
+
186
+ def to_ruby(value)
187
+ value.respond_to?(:to_time) ? value.to_time : Time.at(value).utc
188
+ end
189
+
190
+ alias to_db to_ruby
191
+ end
192
+ end
193
+
194
+ class BooleanConverter < BaseConverter
195
+ FALSE_VALUES = %w(n N no No NO false False FALSE off Off OFF f F).to_set
196
+
197
+ class << self
198
+ def converted?(value)
199
+ converted_values.include?(value)
200
+ end
201
+
202
+ def converted_values
203
+ [true, false]
204
+ end
205
+
206
+ def db_type
207
+ ActiveGraph::Shared::Boolean
208
+ end
209
+
210
+ alias convert_type db_type
211
+
212
+ def to_db(value)
213
+ return false if FALSE_VALUES.include?(value)
214
+ case value
215
+ when TrueClass, FalseClass
216
+ value
217
+ when Numeric, /^\-?[0-9]/
218
+ !value.to_f.zero?
219
+ else
220
+ value.present?
221
+ end
222
+ end
223
+
224
+ alias to_ruby to_db
225
+ end
226
+ end
227
+
228
+ # Converts hash to/from YAML
229
+ class YAMLConverter < BaseConverter
230
+ class << self
231
+ def convert_type
232
+ Hash
233
+ end
234
+
235
+ def db_type
236
+ String
237
+ end
238
+
239
+ def to_db(value)
240
+ Psych.dump(value)
241
+ end
242
+
243
+ def to_ruby(value)
244
+ value.is_a?(Hash) ? value : Psych.load(value)
245
+ end
246
+ end
247
+ end
248
+
249
+ # Converts hash to/from JSON
250
+ class JSONConverter < BaseConverter
251
+ class << self
252
+ def converted?(_value)
253
+ false
254
+ end
255
+
256
+ def convert_type
257
+ JSON
258
+ end
259
+
260
+ def db_type
261
+ String
262
+ end
263
+
264
+ def to_db(value)
265
+ value.to_json
266
+ end
267
+
268
+ def to_ruby(value)
269
+ JSON.parse(value, quirks_mode: true)
270
+ end
271
+ end
272
+ end
273
+
274
+ class EnumConverter
275
+ def initialize(enum_keys, options)
276
+ @enum_keys = enum_keys
277
+ @options = options
278
+
279
+ return unless @options[:case_sensitive].nil?
280
+
281
+ @options[:case_sensitive] = ActiveGraph::Config.enums_case_sensitive
282
+ end
283
+
284
+ def converted?(value)
285
+ value.is_a?(db_type)
286
+ end
287
+
288
+ def supports_array?
289
+ true
290
+ end
291
+
292
+ def db_type
293
+ Integer
294
+ end
295
+
296
+ def convert_type
297
+ Symbol
298
+ end
299
+
300
+ def to_ruby(value)
301
+ @enum_keys.key(value) unless value.nil?
302
+ end
303
+
304
+ alias call to_ruby
305
+
306
+ def to_db(value)
307
+ if value.is_a?(Array)
308
+ value.map(&method(:to_db))
309
+ elsif @options[:case_sensitive]
310
+ @enum_keys[value.to_s.to_sym] ||
311
+ fail(ActiveGraph::Shared::Enum::InvalidEnumValueError, 'Value passed to an enum property must match one of the enum keys')
312
+ else
313
+ @enum_keys[value.to_s.downcase.to_sym] ||
314
+ fail(ActiveGraph::Shared::Enum::InvalidEnumValueError, 'Case-insensitive (downcased) value passed to an enum property must match one of the enum keys')
315
+ end
316
+ end
317
+ end
318
+
319
+ class ObjectConverter < BaseConverter
320
+ class << self
321
+ def convert_type
322
+ Object
323
+ end
324
+
325
+ def to_ruby(value)
326
+ value
327
+ end
328
+ end
329
+ end
330
+
331
+ # Modifies a hash's values to be of types acceptable to Neo4j or matching what the user defined using `type` in property definitions.
332
+ # @param [ActiveGraph::Shared::Property] obj A node or rel that mixes in the Property module
333
+ # @param [Symbol] medium Indicates the type of conversion to perform.
334
+ # @param [Hash] properties A hash of symbol-keyed properties for conversion.
335
+ def convert_properties_to(obj, medium, properties)
336
+ direction = medium == :ruby ? :to_ruby : :to_db
337
+ properties.to_h.each_pair do |key, value|
338
+ next if skip_conversion?(obj, key, value)
339
+
340
+ converted_value = convert_property(key, value, direction)
341
+ if properties.is_a?(ActiveGraph::AttributeSet)
342
+ properties.write_cast_value(key, converted_value)
343
+ else
344
+ properties[key] = converted_value
345
+ end
346
+ end
347
+ end
348
+
349
+ # Converts a single property from its current format to its db- or Ruby-expected output type.
350
+ # @param [Symbol] key A property declared on the model
351
+ # @param value The value intended for conversion
352
+ # @param [Symbol] direction Either :to_ruby or :to_db, indicates the type of conversion to perform
353
+ def convert_property(key, value, direction)
354
+ converted_property(primitive_type(key.to_sym), value, direction)
355
+ end
356
+
357
+ def supports_array?(key)
358
+ type = primitive_type(key.to_sym)
359
+ type.respond_to?(:supports_array?) && type.supports_array?
360
+ end
361
+
362
+ def typecaster_for(value)
363
+ ActiveGraph::Shared::TypeConverters.typecaster_for(value)
364
+ end
365
+
366
+ def typecast_attribute(typecaster, value)
367
+ ActiveGraph::Shared::TypeConverters.typecast_attribute(typecaster, value)
368
+ end
369
+
370
+ private
371
+
372
+ def converted_property(type, value, direction)
373
+ return nil if value.nil?
374
+ CONVERTERS[type] || type.respond_to?(:db_type) ? TypeConverters.to_other(direction, value, type) : value
375
+ end
376
+
377
+ # If the attribute is to be typecast using a custom converter, which converter should it use? If no, returns the type to find a native serializer.
378
+ def primitive_type(attr)
379
+ case
380
+ when serialized_properties.include?(attr)
381
+ serialized_properties[attr]
382
+ when magic_typecast_properties.include?(attr)
383
+ magic_typecast_properties[attr]
384
+ else
385
+ fetch_upstream_primitive(attr)
386
+ end
387
+ end
388
+
389
+ # Returns true if the property isn't defined in the model or if it is nil
390
+ def skip_conversion?(obj, attr, value)
391
+ value.nil? || !obj.class.attributes.key?(attr)
392
+ end
393
+
394
+ class << self
395
+ def included(_)
396
+ ActiveGraph::Shared::TypeConverters.constants.each do |constant_name|
397
+ constant = ActiveGraph::Shared::TypeConverters.const_get(constant_name)
398
+ register_converter(constant) if constant.respond_to?(:convert_type)
399
+ end
400
+ end
401
+
402
+ def typecast_attribute(typecaster, value)
403
+ fail ArgumentError, "A typecaster must be given, #{typecaster} is invalid" unless typecaster.respond_to?(:to_ruby)
404
+ return value if value.nil?
405
+ typecaster.to_ruby(value)
406
+ end
407
+
408
+ def typecaster_for(primitive_type)
409
+ return nil if primitive_type.nil?
410
+ CONVERTERS[primitive_type]
411
+ end
412
+
413
+ # @param [Symbol] direction either :to_ruby or :to_other
414
+ def to_other(direction, value, type)
415
+ fail "Unknown direction given: #{direction}" unless direction == :to_ruby || direction == :to_db
416
+ found_converter = converter_for(type)
417
+ return value unless found_converter
418
+ return value if direction == :to_db && formatted_for_db?(found_converter, value)
419
+ found_converter.send(direction, value)
420
+ end
421
+
422
+ def converter_for(type)
423
+ type.respond_to?(:db_type) ? type : CONVERTERS[type]
424
+ end
425
+
426
+ # Attempts to determine whether conversion should be skipped because the object is already of the anticipated output type.
427
+ # @param [#convert_type] found_converter An object that responds to #convert_type, hinting that it is a type converter.
428
+ # @param value The value for conversion.
429
+ def formatted_for_db?(found_converter, value)
430
+ return false unless found_converter.respond_to?(:db_type)
431
+ found_converter.respond_to?(:converted?) ? found_converter.converted?(value) : value.is_a?(found_converter.db_type)
432
+ end
433
+
434
+ def register_converter(converter)
435
+ CONVERTERS[converter.convert_type] = converter
436
+ end
437
+ end
438
+ end
439
+ end