neon 0.1.0

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.
@@ -0,0 +1,56 @@
1
+ module Neon
2
+ module TransactionHelpers
3
+ module Rest
4
+ def run_rest_transaction(method, *args, &block)
5
+ if @session.auto_tx
6
+ yield
7
+ else
8
+ # Fetch the query for this method
9
+ query = query_for method, *args
10
+ tx = @session.begin_tx
11
+ result = tx.run_query query
12
+ tx.success
13
+ tx.close
14
+ result = parse_result(result, method, *args)
15
+ end
16
+ end
17
+ end
18
+
19
+ module Embedded
20
+ # Used by objects to run a block of code inside a fresh transaction associated
21
+ def run_in_transaction(&block)
22
+ # Retrieve appropriate session based on current type
23
+ # REST:
24
+ # Session: self
25
+ # Entity: @session
26
+ # Embedded:
27
+ # Session: self
28
+ # Entity: get_graph_database
29
+ if respond_to?(:get_graph_database)
30
+ begin
31
+ tx = get_graph_database.begin_tx
32
+ result = yield if block_given?
33
+ tx.success
34
+ tx.close
35
+ rescue Exception => e
36
+ # Roll back the transaction
37
+ tx.failure
38
+ tx.close
39
+ raise e # Let the exception bubble up
40
+ end
41
+ else
42
+ session = @session || self
43
+ result = if session.auto_tx
44
+ r = Transaction.run(session, &block)
45
+ r.pop
46
+ r = r.pop if r.length == 1
47
+ r
48
+ else
49
+ yield if block_given?
50
+ end
51
+ end
52
+ result
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ require "helpers/argument_helpers"
2
+ require "helpers/transaction_helpers"
3
+ require "neon/session"
4
+ require "neon/property_container"
5
+ require "neon/node"
6
+ require "neon/node/rest"
7
+ require "neon/relationship"
8
+ require "neon/relationship/rest"
9
+ require "neon/transaction"
10
+ require "neon/transaction/placebo"
11
+ require "neon/transaction/rest"
12
+
13
+ # If the platform is Java then load all java related files.
14
+ if RUBY_PLATFORM == 'java'
15
+ require "java"
16
+ require "neo4j-community"
17
+ require "neon/node/embedded"
18
+ require "neon/relationship/embedded"
19
+ end
20
+
21
+ # @author Ujjwal Thaakar
22
+ module Neon
23
+ end
@@ -0,0 +1,8 @@
1
+ source 'http://rubygems.org'
2
+ ruby 'rbx'
3
+
4
+ # The API is written in grap
5
+ gem 'grape'
6
+
7
+ # Use Puma for the web server
8
+ gem 'puma'
@@ -0,0 +1,48 @@
1
+ require "helpers/argument_helpers"
2
+
3
+ module Neon
4
+ module Node
5
+ extend ArgumentHelpers
6
+
7
+ class << self
8
+ # Creates a new Node in the database. All subsequent changes are immediately persisted.
9
+ #
10
+ # @overload new(attributes, labels, session)
11
+ # @param attributes [Hash] the properties to initialize the node with.
12
+ # @param labels [String, Symbol, Array<String, Symbol>] an optional list of labels or an array of labels. Labels can be strings or symbols.
13
+ # @param session [Session] an optional session can be provided as the last value to indicate the database where to create the node.
14
+ # If none is provided then the current session is assumed.
15
+ #
16
+ # @return [Node] a new node.
17
+ def new(attributes, *args)
18
+ session = extract_session(args)
19
+ labels = args.flatten
20
+ begin
21
+ session.create_node(attributes, labels)
22
+ rescue NoMethodError => e
23
+ _raise_invalid_session_error(session, e)
24
+ end
25
+ end
26
+
27
+ # Loads an existing node with the given id
28
+ #
29
+ # @param id [Integer] the id of the node to be loaded and returned.
30
+ # @param session [Session] an optional session from where to load the node.
31
+ #
32
+ # @return [Node] an existing node with the given id and specified session. It returns nil if the node is not found.
33
+ def load(id, session = Neon::Session.current)
34
+ begin
35
+ session.load(id)
36
+ rescue NoMethodError => e
37
+ _raise_invalid_session_error(session, e)
38
+ end
39
+ end
40
+
41
+ private
42
+ def _raise_invalid_session_error(session, e)
43
+ STDERR.puts e
44
+ raise Neon::Session::InvalidSessionTypeError.new(session.class)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ # Extend the Java NodeProxy
2
+ Java::OrgNeo4jKernelImplCore::NodeProxy.class_eval do
3
+ include Neon::PropertyContainer::Embedded
4
+ include Neon::TransactionHelpers
5
+
6
+ def to_s
7
+ "Embedded Node[#{getId}]"
8
+ end
9
+
10
+ def create_rel_to(end_node, name, attributes = {})
11
+ run_in_transaction { _create_rel_to end_node, name, attributes }
12
+ end
13
+
14
+ private
15
+ def _destroy
16
+ get_relationships.each { |r| r.delete }
17
+ delete
18
+ end
19
+
20
+ def _create_rel_to(end_node, name, attributes)
21
+ return nil if get_graph_database != end_node.get_graph_database
22
+ type = Java::OrgNeo4jGraphdb::DynamicRelationshipType.with_name(name)
23
+ rel = create_relationship_to(end_node, type)
24
+ if (rel.isType(type))
25
+ rel.props = attributes
26
+ rel
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,109 @@
1
+ require "neon/property_container"
2
+
3
+ module Neon
4
+ module Node
5
+ class Rest
6
+ include PropertyContainer::Rest
7
+ attr_reader :session # The Rest session this node belongs to.
8
+ attr_reader :id # The neo id of this node.
9
+ attr_reader :node # The neography hash containing information about the node.
10
+
11
+ # Initialize the node with a neography node and a REST session
12
+ #
13
+ # @param node [Hash] a neogrpahy node hash.
14
+ # @param session [Session::Rest] the session this node was initialized to.
15
+ #
16
+ # @return [Node::Rest] a new rest node.
17
+ def initialize(node, session)
18
+ @session = session # Set the session
19
+ @node = node # Set the node
20
+ @id = node["self"].split('/').last.to_i # Set the id
21
+ end
22
+
23
+ def to_s
24
+ "REST Node[#{@id}]"
25
+ end
26
+
27
+ # Create a unidirectional relationship starting from this node to another node.
28
+ #
29
+ # @param end_node [Node::Rest] the end node for the unidirectional relationship.
30
+ # @param type [String, Symbol] the type of this relationship.
31
+ # @param attributes [Hash] a hash of the initial property-value pairs.
32
+ #
33
+ # @return [Relationship::Rest] a new relationship between *start_node* and *end_node*.
34
+ def create_rel_to(end_node, type, attributes = {})
35
+ return nil if @session.url != end_node.session.url
36
+ attributes.delete_if { |key, value| value.nil? }
37
+ neo_rel = @session.neo.create_relationship(type, @node, end_node.node, attributes)
38
+ return nil if neo_rel.nil?
39
+ rel = Relationship::Rest.new(neo_rel, @session)
40
+ rescue NoMethodError => e
41
+ _raise_doesnt_exist_anymore_error(e)
42
+ end
43
+
44
+ # Move to a separate file
45
+ QUERIES = {
46
+ :[] => lambda do |node, *keys|
47
+ query = "START n = node({id})\nWITH "
48
+ query << keys.map { |key| "n.#{key.to_s.strip} as #{key.to_s.strip}" }.join(", ")
49
+ query << "\nRETURN "
50
+ query << keys.map { |key| key.to_s.strip }.join(", ")
51
+ [query, {id: node.id}]
52
+ end
53
+ }
54
+
55
+ def query_for(method, *args)
56
+ # Fetch appropriate query and covert the args to a hash corresponding the query parameters
57
+ QUERIES[method].call(self, *args)
58
+ end
59
+
60
+ # Move to a seprate file
61
+ RESULT_PARSER = {
62
+ :[] => lambda do |result, *keys|
63
+ parsed_result = []
64
+ result = result.first
65
+ columns = result["columns"]
66
+ data = result["data"].first["row"].dup
67
+ raise "Corrupted result" if columns.length != keys.length
68
+ for i in 0...keys.length
69
+ parsed_result << if keys[i] == columns[i]
70
+ data.shift
71
+ else
72
+ nil
73
+ end
74
+ end
75
+ if keys.length == 1
76
+ parsed_result.pop
77
+ else
78
+ parsed_result
79
+ end
80
+ end
81
+ }
82
+
83
+ def parse_result(result, method, *args)
84
+ RESULT_PARSER[method].call(result, *args)
85
+ end
86
+
87
+ private
88
+ def _get_properties(*keys)
89
+ @session.neo.get_node_properties(@node, *keys)
90
+ end
91
+
92
+ def _reset_properties(attributes)
93
+ @session.neo.reset_node_properties(@node, attributes)
94
+ end
95
+
96
+ def _set_private_vars_to_nil
97
+ @node = @session = nil
98
+ end
99
+
100
+ def _delete
101
+ @session.neo.delete_node(@node)
102
+ end
103
+
104
+ def _destroy
105
+ @session.neo.delete_node!(@node)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,277 @@
1
+ module Neon
2
+ # A module to contain REST and Embedded implementation for a property container namely nodes and relationships
3
+ # @author Ujjwal Thaakar
4
+ module PropertyContainer
5
+ # Server implementation for a property container
6
+ module Rest
7
+ include TransactionHelpers::Rest
8
+ # Compares to anothe property container
9
+ #
10
+ # @param other [PropertyContainer] the other property container being compared to
11
+ #
12
+ # @return [Boolean] wether both are the same entities based on their ids and sessions
13
+ def ==(other)
14
+ @id == other.id && @session == other.session
15
+ end
16
+
17
+ # Fetch one or more properties e.g. node[:property, :another_Property]. Non existent keys return nil.
18
+ #
19
+ # @param keys [Array<String, Symbol>] the properties to return
20
+ #
21
+ # @return [Array<String>, String] an array of the values of the properties, sorted in the same order they were queried.
22
+ # In case only a single property is fetche e.g. node[ :property] it returns a String containing the corresponding value.
23
+ def [](*keys)
24
+ # Lesson Learnt
25
+ # ==============
26
+ # Don't change what doesn't belong to you
27
+ keys = keys.map(&:to_s)
28
+ run_in_transaction(:[], *keys) do
29
+ properties = props # Fetch all properties as this is more efficient than firing a HTTP request for every key
30
+ result = []
31
+ keys.each { |k| result << properties[k] }
32
+ # If a single key was asked then return it's value else return an array of values in the correct order
33
+ if keys.length == 1
34
+ result.first
35
+ else
36
+ result
37
+ end
38
+ end
39
+ rescue NoMethodError => e
40
+ _raise_doesnt_exist_anymore_error(e)
41
+ end
42
+
43
+ # Set one or more properties e.g. node[:property, :another_property] = 5, "Neo4J". nil keys are ignored.
44
+ #
45
+ # @param keys [Array<String, Symbol>] the properties to set.
46
+ # @param values [Array<Numeric, String, Symbol, Array<Numeric, String, Symbol>>] the value to assign to the properties in the order specified.
47
+ #
48
+ # @return [void]
49
+ def []=(*keys, values)
50
+ # Flattent the values to 1 level. This creates an arrray of values in the case only a single value is provided.
51
+ values = [values].flatten(1)
52
+ keys = keys.map(&:to_s)
53
+ run_in_transaction(:[]=, *keys, values) do
54
+ properties = props
55
+ Hash[keys.zip(values)].each { |k, v| properties[k] = v unless k.nil? }
56
+ self.props = properties # Reset all the properties - write simple inefficient code until it proves inefficient
57
+ end
58
+ rescue NoMethodError => e
59
+ _raise_doesnt_exist_anymore_error(e)
60
+ end
61
+
62
+ # Return all properties of the property container.
63
+ #
64
+ # @return [Hash] a hash of all properties and their values.
65
+ def props
66
+ run_in_transaction(:props) do
67
+ _get_properties || {}
68
+ end
69
+ rescue NoMethodError => e
70
+ _raise_doesnt_exist_anymore_error(e)
71
+ end
72
+
73
+ # Reset all properties of the property container.
74
+ #
75
+ # @param attributes [Hash] a hash of the key-value pairs to set.
76
+ #
77
+ # @return [void]
78
+ def props=(attributes)
79
+ attributes.delete_if { |key, value| key.nil? || value.nil? } # Remove keys-value pairs where either is nil
80
+ run_in_transaction(:props=, attributes) do
81
+ _reset_properties(attributes)
82
+ return
83
+ end
84
+ rescue NoMethodError => e
85
+ _raise_doesnt_exist_anymore_error(e)
86
+ end
87
+
88
+ # Delete this entity.
89
+ def del
90
+ run_in_transaction(:del) do
91
+ _delete
92
+ _set_private_vars_to_nil
93
+ end
94
+ rescue NoMethodError => e
95
+ _raise_doesnt_exist_anymore_error(e)
96
+ end
97
+
98
+ # Destroy this entity i.e. delete it and it's associated entities e.g. relationships of a node
99
+ # and in case of relationships, both its nodes.
100
+ def destroy
101
+ run_in_transaction(:destroy) do
102
+ _destroy # Delete the entity after deleting connected entities
103
+ _set_private_vars_to_nil
104
+ end
105
+ rescue NoMethodError => e
106
+ _raise_doesnt_exist_anymore_error(e)
107
+ end
108
+
109
+ private
110
+ def _raise_doesnt_exist_anymore_error(e)
111
+ unless @session.nil?
112
+ STDERR.puts e
113
+ raise e
114
+ end
115
+ end
116
+
117
+ def _abstract
118
+ raise "No properties"
119
+ end
120
+
121
+ alias :_get_properties :_abstract
122
+ alias :_reset_properties :_abstract
123
+ alias :_set_private_vars_to_nil :_abstract
124
+ alias :_delete :_abstract
125
+ alias :_destroy :_abstract
126
+ alias :query_for :_abstract
127
+ alias :parse_result :_abstract
128
+ end
129
+
130
+ # Embedded implementation for a property container
131
+ module Embedded
132
+ include TransactionHelpers::Embedded
133
+ def self.included(klazz)
134
+ raise "Cannot include PropertyContainer::Embedded without JRuby" unless RUBY_PLATFORM == 'java'
135
+ end
136
+
137
+ # @return [FixNum] the id of the entity.
138
+ def id
139
+ get_id
140
+ end
141
+
142
+ # Compares to anothe property container
143
+ #
144
+ # @param other [PropertyContainer] the other property container being compared to
145
+ #
146
+ # @return [Boolean] wether both are the same entities based on their ids and sessions
147
+ def ==(other)
148
+ id == other.id
149
+ end
150
+
151
+ # Fetch one or more properties e.g. node[:property, :another_Property]. Non existent keys return nil.
152
+ #
153
+ # @param keys [Array<String, Symbol>] the properties to return
154
+ #
155
+ # @return [Array<String>, String] an array of the values of the properties, sorted in the same order they were queried.
156
+ # In case only a single property is fetche e.g. node[ :property] it returns a String containing the corresponding value.
157
+ def [](*keys)
158
+ run_in_transaction do
159
+ keys = _serialize(keys)
160
+ result = []
161
+ keys.each do |k|
162
+ # Result contains the values for the keys asked or nil if they key does not exist
163
+ result << if has_property(k)
164
+ get_property(k)
165
+ else
166
+ nil
167
+ end
168
+ end
169
+ # If a single key was asked then return it's value else return an array of values in the correct order
170
+ if keys.length == 1
171
+ result.first
172
+ else
173
+ result
174
+ end
175
+ end
176
+ end
177
+
178
+ # Set one or more properties e.g. node[:property, :another_property] = 5, "Neo4J". nil keys are ignored.
179
+ #
180
+ # @param keys [Array<String, Symbol>] the properties to set.
181
+ # @param values [Array<Numeric, String, Symbol, Array<Numeric, String, Symbol>>] the value to assign to the properties in the order specified.
182
+ #
183
+ # @return [void]
184
+ def []=(*keys, values)
185
+ run_in_transaction do
186
+ # Flattent the values to 1 level. This creates an arrray of values in the case only a single value is provided.
187
+ values = [values].flatten(1)
188
+ attributes = Hash[keys.zip values]
189
+ nil_values = lambda { |_, v| v.nil? } # Resusable lambda
190
+ keys_to_delete = attributes.select(&nil_values).keys # Get the keys to be removed
191
+ attributes.delete_if(&nil_values) # Now remove those keys from attributes
192
+ keys_to_delete.each { |k| remove_property(k) if has_property(k) } # Remove the keys to be deleted if they are valid
193
+ attributes = _serialize(attributes)
194
+ attributes.each { |k, v| set_property(k, v) } # Set key-value pairs for remaining attributes
195
+ end
196
+ end
197
+
198
+ # Return all properties of the property container.
199
+ #
200
+ # @return [Hash] a hash of all properties and their values.
201
+ def props
202
+ run_in_transaction do
203
+ result = {} # Initialize results
204
+ get_property_keys.each { |key| result[key] = get_property(key) } # Populate the hash with the container's key-value pairs
205
+ result # Return the result
206
+ end
207
+ end
208
+
209
+ # Reset all properties of the property container.
210
+ #
211
+ # @param attributes [Hash] a hash of the key-value pairs to set.
212
+ #
213
+ # @return [void]
214
+ def props=(attributes)
215
+ run_in_transaction do
216
+ attributes.delete_if { |key, value| key.nil? || value.nil? } # Remove keys-value pairs where either is nil before serialization
217
+ attributes = _serialize(attributes)
218
+ get_property_keys.each { |key| remove_property(key) } # Remove all properties
219
+ attributes.each { |key, value| set_property(key, value) } # Set key-value pairs
220
+ end
221
+ end
222
+
223
+ # Delete this entity
224
+ def del
225
+ run_in_transaction { delete }
226
+ end
227
+
228
+ # Destroy this entity i.e. delete it and it's associated entities e.g. relationships of a node
229
+ # and in case of relationships, both its nodes.
230
+ def destroy
231
+ run_in_transaction { _destroy }
232
+ end
233
+
234
+ private
235
+ # Serialize keys and values into approiate type for conversion to Java objects
236
+ def _serialize(*objects)
237
+ result = objects.map do |obj|
238
+ if obj.is_a?(Hash)
239
+ _serialize_hash(obj)
240
+ else
241
+ _appropriate_type_for obj
242
+ end
243
+ end
244
+ if objects.length == 1
245
+ result.first
246
+ else
247
+ result
248
+ end
249
+ end
250
+
251
+ # Convert all keys to strings and values to an appropriate type
252
+ def _serialize_hash(hash)
253
+ attributes = {}
254
+ hash.each { |key, value| attributes[key.to_s] = _appropriate_type_for value }
255
+ attributes
256
+ end
257
+
258
+ def _appropriate_type_for(value)
259
+ case value
260
+ when String, Numeric, TrueClass, FalseClass
261
+ value # Will be converted by JRuby
262
+ when Array
263
+ # Convert each of the elements to an appropriate type
264
+ # Convert to a Java array of the first element's type. If the types of all elements don't match then the runtime raises an exception.
265
+ result = value.map { |v| _appropriate_type_for v }
266
+ result.to_java(result.first.class.to_s.downcase.to_sym)
267
+ else
268
+ value.to_s # Try and convert to a string
269
+ end
270
+ end
271
+
272
+ def _destroy
273
+ raise "No properties"
274
+ end
275
+ end
276
+ end
277
+ end