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,204 @@
1
+ module ActiveGraph::Shared
2
+ # The DeclaredPropertyuManager holds details about objects created as a result of calling the #property
3
+ # class method on a class that includes ActiveGraph::Node or ActiveGraph::Relationship. There are many options
4
+ # that are referenced frequently, particularly during load and save, so this provides easy access and
5
+ # a way of separating behavior from the general Active{obj} modules.
6
+ #
7
+ # See ActiveGraph::Shared::DeclaredProperty for definitions of the property objects themselves.
8
+ class DeclaredProperties
9
+ include ActiveGraph::Shared::TypeConverters
10
+
11
+ attr_reader :klass
12
+ delegate :each, :each_pair, :each_key, :each_value, to: :registered_properties
13
+
14
+ # Each class that includes ActiveGraph::Node or ActiveGraph::Relationship gets one instance of this class.
15
+ # @param [#declared_properties] klass An object that has the #declared_properties method.
16
+ def initialize(klass)
17
+ @klass = klass
18
+ end
19
+
20
+ def [](key)
21
+ registered_properties[key.to_sym]
22
+ end
23
+
24
+ def property?(key)
25
+ registered_properties.key?(key.to_sym)
26
+ end
27
+
28
+ # @param [ActiveGraph::Shared::DeclaredProperty] property An instance of DeclaredProperty, created as the result of calling
29
+ # #property on an Node or Relationship class. The DeclaredProperty has specifics about the property, but registration
30
+ # makes the management object aware of it. This is necessary for type conversion, defaults, and inclusion in the nil and string hashes.
31
+ def register(property)
32
+ @_attributes_nil_hash = nil
33
+ @_attributes_string_map = nil
34
+ registered_properties[property.name] = property
35
+ register_magic_typecaster(property) if property.magic_typecaster
36
+ declared_property_defaults[property.name] = property.default_value if !property.default_value.nil?
37
+ end
38
+
39
+ def index_or_fail!(key, id_property_name, type = :exact)
40
+ return if key == id_property_name
41
+ fail "Cannot index undeclared property #{key}" unless property?(key)
42
+ registered_properties[key].index!(type)
43
+ end
44
+
45
+ def constraint_or_fail!(key, id_property_name, type = :unique)
46
+ return if key == id_property_name
47
+ fail "Cannot constraint undeclared property #{property}" unless property?(key)
48
+ registered_properties[key].constraint!(type)
49
+ end
50
+
51
+ # The :default option in ActiveGraph::Node#property class method allows for setting a default value instead of
52
+ # nil on declared properties. This holds those values.
53
+ def declared_property_defaults
54
+ @_default_property_values ||= {}
55
+ end
56
+
57
+ def registered_properties
58
+ @_registered_properties ||= {}
59
+ end
60
+
61
+ def indexed_properties
62
+ registered_properties.select { |_, p| p.index_or_constraint? }
63
+ end
64
+
65
+ # During object wrap, a hash is needed that contains each declared property with a nil value.
66
+ # The active_attr dependency is capable of providing this but it is expensive and calculated on the fly
67
+ # each time it is called. Rather than rely on that, we build this progressively as properties are registered.
68
+ # When the node or rel is loaded, this is used as a template.
69
+ def attributes_nil_hash
70
+ @_attributes_nil_hash ||= {}.tap do |attr_hash|
71
+ registered_properties.each_pair do |k, prop_obj|
72
+ val = prop_obj.default_value
73
+ attr_hash[k.to_s] = val
74
+ end
75
+ end.freeze
76
+ end
77
+
78
+ # During object wrapping, a props hash is built with string keys but ActiveGraph::Core provides symbols.
79
+ # Rather than a `to_s` or `symbolize_keys` during every load, we build a map of symbol-to-string
80
+ # to speed up the process. This increases memory used by the gem but reduces object allocation and GC, so it is faster
81
+ # in practice.
82
+ def attributes_string_map
83
+ @_attributes_string_map ||= {}.tap do |attr_hash|
84
+ attributes_nil_hash.each_key { |k| attr_hash[k.to_sym] = k }
85
+ end.freeze
86
+ end
87
+
88
+ # @param [Symbol] k A symbol for which the String representation is sought. This might seem silly -- we could just call #to_s --
89
+ # but when this happens many times while loading many objects, it results in a surprisingly significant slowdown.
90
+ # The branching logic handles what happens if a property can't be found.
91
+ # The first option attempts to find it in the existing hash.
92
+ # The second option checks whether the key is the class's id property and, if it is, the string hash is rebuilt with it to prevent
93
+ # future lookups.
94
+ # The third calls `to_s`. This would happen if undeclared properties are found on the object. We could add them to the string map
95
+ # but that would result in unchecked, un-GCed memory consumption. In the event that someone is adding properties dynamically,
96
+ # maybe through user input, this would be bad.
97
+ def string_key(k)
98
+ attributes_string_map[k] || string_map_id_property(k) || k.to_s
99
+ end
100
+
101
+ def unregister(name)
102
+ # might need to be include?(name.to_s)
103
+ fail ArgumentError, "Argument `#{name}` not an attribute" if not registered_properties[name]
104
+ registered_properties.delete(name)
105
+ unregister_magic_typecaster(name)
106
+ unregister_property_default(name)
107
+ end
108
+
109
+ def serialize(name, coder = JSON)
110
+ @serialize ||= {}
111
+ @serialize[name] = coder
112
+ end
113
+
114
+ def serialized_properties=(serialize_hash)
115
+ @serialized_property_keys = nil
116
+ @serialize = serialize_hash.clone
117
+ end
118
+
119
+ def serialized_properties
120
+ @serialize ||= {}
121
+ end
122
+
123
+ def serialized_properties_keys
124
+ @serialized_property_keys ||= serialized_properties.keys
125
+ end
126
+
127
+ def magic_typecast_properties_keys
128
+ @magic_typecast_properties_keys ||= magic_typecast_properties.keys
129
+ end
130
+
131
+ def magic_typecast_properties
132
+ @magic_typecast_properties ||= {}
133
+ end
134
+
135
+ EXCLUDED_TYPES = [Array, Range, Regexp]
136
+ def value_for_where(key, value)
137
+ return value unless prop = registered_properties[key]
138
+ return value_for_db(key, value) if prop.typecaster && prop.typecaster.convert_type == value.class
139
+
140
+ if should_convert_for_where?(key, value)
141
+ value_for_db(key, value)
142
+ else
143
+ value
144
+ end
145
+ end
146
+
147
+ def value_for_db(key, value)
148
+ return value unless registered_properties[key]
149
+ convert_property(key, value, :to_db)
150
+ end
151
+
152
+ def value_for_ruby(key, value)
153
+ return unless registered_properties[key]
154
+ convert_property(key, value, :to_ruby)
155
+ end
156
+
157
+ def inject_defaults!(object, props)
158
+ declared_property_defaults.each_pair do |k, v|
159
+ props[k.to_sym] = v.respond_to?(:call) ? v.call : v if object.send(k).nil? && props[k.to_sym].nil?
160
+ end
161
+ props
162
+ end
163
+
164
+ protected
165
+
166
+ # Prevents repeated calls to :_attribute_type, which isn't free and never changes.
167
+ def fetch_upstream_primitive(attr)
168
+ registered_properties[attr].type
169
+ end
170
+
171
+ private
172
+
173
+ def should_convert_for_where?(key, value)
174
+ (value.is_a?(Array) && supports_array?(key)) || !EXCLUDED_TYPES.include?(value.class)
175
+ end
176
+
177
+ # @param [Symbol] key An undeclared property value found in the _persisted_obj.properties hash.
178
+ # Typically, this is a node's id property, which will not be registered as other properties are.
179
+ # In the future, this should probably be reworked a bit. This class should either not know or care
180
+ # about the id property or it should be in charge of it. In the meantime, this improves
181
+ # node load performance.
182
+ def string_map_id_property(key)
183
+ return unless klass.id_property_name == key
184
+ key.to_s.tap do |string_key|
185
+ @_attributes_string_map = attributes_string_map.dup.tap { |h| h[key] = string_key }.freeze
186
+ end
187
+ end
188
+
189
+ def unregister_magic_typecaster(property)
190
+ magic_typecast_properties.delete(property)
191
+ @magic_typecast_properties_keys = nil
192
+ end
193
+
194
+ def unregister_property_default(property)
195
+ declared_property_defaults.delete(property)
196
+ @_default_property_values = nil
197
+ end
198
+
199
+ def register_magic_typecaster(property)
200
+ magic_typecast_properties[property.name] = property.magic_typecaster
201
+ @magic_typecast_properties_keys = nil
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,109 @@
1
+ module ActiveGraph::Shared
2
+ # Contains methods related to the management
3
+ class DeclaredProperty
4
+ include Comparable
5
+
6
+ class IllegalPropertyError < ActiveGraph::Error; end
7
+ include ActiveGraph::Shared::DeclaredProperty::Index
8
+
9
+ ILLEGAL_PROPS = %w(from_node to_node start_node end_node)
10
+ attr_reader :name, :name_string, :name_sym, :options, :magic_typecaster, :type, :typecaster, :default_value
11
+ alias default default_value
12
+
13
+ def initialize(name, options = {})
14
+ fail IllegalPropertyError, "#{name} is an illegal property" if ILLEGAL_PROPS.include?(name.to_s)
15
+ fail TypeError, "can't convert #{name.class} into Symbol" unless name.respond_to?(:to_sym)
16
+ @name = @name_sym = name.to_sym
17
+ @name_string = name.to_s
18
+ @type = options[:type]
19
+ @typecaster = options[:typecaster]
20
+ @default_value = options[:default]
21
+ @options = options
22
+ fail_invalid_options!
23
+ end
24
+
25
+ # Compare attribute definitions
26
+ #
27
+ # @example
28
+ # attribute_definition <=> other
29
+ #
30
+ # @param [ActiveGraph::Shared::DeclaredProperty, Object] other The other
31
+ # attribute definition to compare with.
32
+ #
33
+ # @return [-1, 0, 1, nil]
34
+ def <=>(other)
35
+ return nil unless other.instance_of? self.class
36
+ return nil if name == other.name && options != other.options
37
+ self.to_s <=> other.to_s
38
+ end
39
+
40
+ def inspect
41
+ options_description = options.map { |key, value| "#{key.inspect} => #{value.inspect}" }.sort.join(', ')
42
+ inspected_options = ", #{options_description}" unless options_description.empty?
43
+ "attribute :#{name}#{inspected_options}"
44
+ end
45
+
46
+ def to_s
47
+ name.to_s
48
+ end
49
+
50
+ def to_sym
51
+ name
52
+ end
53
+
54
+ def [](key)
55
+ respond_to?(key) ? public_send(key) : nil
56
+ end
57
+
58
+ def register
59
+ register_magic_properties
60
+ end
61
+
62
+ def fail_invalid_options!
63
+ case
64
+ when index?(:exact) && constraint?(:unique)
65
+ fail ActiveGraph::InvalidPropertyOptionsError,
66
+ "#Uniqueness constraints also provide exact indexes, cannot set both options on property #{name}"
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def option_with_value!(key, value)
73
+ options[key] = value
74
+ fail_invalid_options!
75
+ end
76
+
77
+ def option_with_value?(key, value)
78
+ options[key] == value
79
+ end
80
+
81
+ # Tweaks properties
82
+ def register_magic_properties
83
+ @type ||= ActiveGraph::Config.timestamp_type if timestamp_prop?
84
+
85
+ register_magic_typecaster
86
+ register_type_converter
87
+ end
88
+
89
+ def timestamp_prop?
90
+ name.to_sym == :created_at || name.to_sym == :updated_at
91
+ end
92
+
93
+ def register_magic_typecaster
94
+ found_typecaster = ActiveGraph::Shared::TypeConverters.typecaster_for(type)
95
+ return unless found_typecaster && found_typecaster.respond_to?(:primitive_type)
96
+ @typecaster = found_typecaster
97
+ @magic_typecaster = type
98
+ @type = found_typecaster.primitive_type
99
+ end
100
+
101
+ def register_type_converter
102
+ converter = options[:serializer]
103
+ return unless converter
104
+ @type = converter.convert_type
105
+ @typecaster = ActiveGraph::Shared::TypeConverters::ObjectConverter
106
+ ActiveGraph::Shared::TypeConverters.register_converter(converter)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,37 @@
1
+ module ActiveGraph::Shared
2
+ class DeclaredProperty
3
+ # None of these methods interact with the database. They only keep track of property settings in models.
4
+ # It could (should?) handle the actual indexing/constraining, but that's TBD.
5
+ module Index
6
+ def index_or_constraint?
7
+ index?(:exact) || constraint?(:unique)
8
+ end
9
+
10
+ def index?(type = :exact)
11
+ options.key?(:index) && options[:index] == type
12
+ end
13
+
14
+ def constraint?(type = :unique)
15
+ options.key?(:constraint) && options[:constraint] == type
16
+ end
17
+
18
+ def index!(type = :exact)
19
+ fail ActiveGraph::InvalidPropertyOptionsError, "Can't set index on constrainted property #{name} (constraints get indexes automatically)" if constraint?(:unique)
20
+ options[:index] = type
21
+ end
22
+
23
+ def constraint!(type = :unique)
24
+ fail ActiveGraph::InvalidPropertyOptionsError, "Can't set constraint on indexed property #{name} (constraints get indexes automatically)" if index?(:exact)
25
+ options[:constraint] = type
26
+ end
27
+
28
+ def unindex!(type = :exact)
29
+ options.delete(:index) if index?(type)
30
+ end
31
+
32
+ def unconstraint!(type = :unique)
33
+ options.delete(:constraint) if constraint?(type)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,167 @@
1
+ module ActiveGraph::Shared
2
+ module Enum
3
+ extend ActiveSupport::Concern
4
+
5
+ class ConflictingEnumMethodError < ActiveGraph::Error; end
6
+ class InvalidEnumValueError < ActiveGraph::InvalidParameterError; end
7
+
8
+ module ClassMethods
9
+ attr_reader :neo4j_enum_data
10
+
11
+ # Similar to ActiveRecord enum, maps an integer value on the database to
12
+ # a set of enum keys.
13
+ #
14
+ # @example Base example
15
+ # class Media
16
+ # include ActiveGraph::Node
17
+ # enum type: [:image, :video, :unknown]
18
+ # end
19
+ # Media.types # => { :images => 0, :video => 1, :unknown => 2 }
20
+ #
21
+ # media.video!
22
+ # media.image? # => false
23
+ # media.type # => :video
24
+ #
25
+ # Media.videos # => All medias with type = 1 (:video)
26
+ # Media.where(type: :video) # => All medias with type = 1 (:video)
27
+ #
28
+ # @example Prefix-ing an enum
29
+ # Media.enum type: [:image, :video, :unknown], _prefix: :enum
30
+ #
31
+ # media.enum_video!
32
+ # media.enum_video? # => true
33
+ #
34
+ # @example Suffix-ing an enum
35
+ # Media.enum type: [:image, :video, :unknown], _suffix: true
36
+ #
37
+ # media.video_type!
38
+ # media.video_type? # => true
39
+ #
40
+ # @example Define a custom mapping for keys-numbers
41
+ # Media.enum type: { image: 1, video: 2, unknown: 3 }
42
+ #
43
+ # @see http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html
44
+ def enum(parameters = {})
45
+ options, parameters = *split_options_and_parameters(parameters)
46
+ parameters.each do |property_name, enum_keys|
47
+ enum_keys = normalize_key_list enum_keys, options
48
+ @neo4j_enum_data ||= {}
49
+ @neo4j_enum_data[property_name] = enum_keys
50
+ define_property(property_name, enum_keys, options)
51
+ define_enum_methods(property_name, enum_keys, options)
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ def normalize_key_list(enum_keys, options)
58
+ case_sensitive = options.fetch(:_case_sensitive, ActiveGraph::Config.enums_case_sensitive)
59
+
60
+ case enum_keys
61
+ when Hash
62
+ enum_keys
63
+ when Array
64
+ enum_keys = Hash[enum_keys.each_with_index.to_a]
65
+ else
66
+ fail ArgumentError, 'Invalid parameter for enum. Please provide an Array or an Hash.'
67
+ end
68
+
69
+ unless case_sensitive
70
+ enum_keys.each_key do |key|
71
+ fail ArgumentError, 'Enum keys must be lowercase unless _case_sensitive = true' unless key.downcase == key
72
+ end
73
+ end
74
+
75
+ enum_keys
76
+ end
77
+
78
+ VALID_OPTIONS_FOR_ENUMS = [:_index, :_prefix, :_suffix, :_default, :_case_sensitive]
79
+
80
+ def split_options_and_parameters(parameters)
81
+ options = {}
82
+ new_parameters = {}
83
+ parameters.each do |k, v|
84
+ if VALID_OPTIONS_FOR_ENUMS.include? k
85
+ options[k] = v
86
+ else
87
+ new_parameters[k] = v
88
+ end
89
+ end
90
+ [options, new_parameters]
91
+ end
92
+
93
+ def define_property(property_name, enum_keys, options)
94
+ property property_name, build_property_options(enum_keys, options)
95
+ serialize property_name, ActiveGraph::Shared::TypeConverters::EnumConverter.new(enum_keys, build_enum_options(enum_keys, options))
96
+
97
+ # If class has already been inherited, make sure subclasses fully inherit enum
98
+ subclasses.each do |klass|
99
+ klass.serialized_properties = self.serialized_properties
100
+ end
101
+ end
102
+
103
+ def build_property_options(_enum_keys, options = {})
104
+ {
105
+ default: options[:_default]
106
+ }
107
+ end
108
+
109
+ def build_enum_options(enum_keys, options = {})
110
+ if options[:_default] && not(enum_keys.include?(options[:_default]))
111
+ fail ArgumentError, 'Enum default value must match an enum key'
112
+ end
113
+
114
+ build_property_options(enum_keys, options).tap do |enum_options|
115
+ enum_options[:case_sensitive] = options[:_case_sensitive]
116
+ end
117
+ end
118
+
119
+ def define_enum_methods(property_name, enum_keys, options)
120
+ define_enum_methods_?(property_name, enum_keys, options)
121
+ define_enum_methods_!(property_name, enum_keys, options)
122
+ define_class_methods(property_name, enum_keys)
123
+ end
124
+
125
+ def define_class_methods(property_name, enum_keys)
126
+ plural_property_name = property_name.to_s.pluralize.to_sym
127
+ define_singleton_method(plural_property_name) do
128
+ enum_keys
129
+ end
130
+ end
131
+
132
+ def define_enum_methods_?(property_name, enum_keys, options)
133
+ enum_keys.each_key do |enum_value|
134
+ method_name = build_method_name(enum_value, property_name, options)
135
+ check_enum_method_conflicts! property_name, :"#{method_name}?"
136
+ define_method("#{method_name}?") do
137
+ __send__(property_name).to_s.to_sym == enum_value
138
+ end
139
+ end
140
+ end
141
+
142
+ def define_enum_methods_!(property_name, enum_keys, options)
143
+ enum_keys.each_key do |enum_value|
144
+ method_name = build_method_name(enum_value, property_name, options)
145
+ check_enum_method_conflicts! property_name, :"#{method_name}!"
146
+ define_method("#{method_name}!") do
147
+ __send__("#{property_name}=", enum_value)
148
+ end
149
+ end
150
+ end
151
+
152
+ def build_method_name(base_name, property_name, options)
153
+ method_name = base_name
154
+ method_name = "#{method_name}_#{property_name}" if options[:_suffix]
155
+ method_name = "#{options[:_prefix]}_#{method_name}" if options[:_prefix]
156
+ method_name
157
+ end
158
+
159
+ def check_enum_method_conflicts!(property_name, method_name)
160
+ fail ConflictingEnumMethodError,
161
+ "The enum `#{property_name}` is trying to define a `#{method_name}` method, "\
162
+ 'that is already defined. Try to use options `:prefix` or `:suffix` '\
163
+ 'to avoid conflicts.' if instance_methods(false).include?(method_name)
164
+ end
165
+ end
166
+ end
167
+ end