benches/strings: add .gitignore and `make clean`
[nit.git] / lib / neo4j / neo4j.nit
index 7d36d56..4694f8d 100644 (file)
@@ -64,6 +64,7 @@
 module neo4j
 
 import curl_json
+import error
 
 # Handles Neo4j server start and stop command
 #
@@ -115,16 +116,15 @@ class Neo4jClient
        init(base_url: String) do
                self.base_url = base_url
                var root = service_root
-               if not root isa JsonObject then
-                       print "Neo4jClientError: cannot connect to server at {base_url}"
-                       abort
+               assert root isa JsonObject else
+                       sys.stderr.write "Neo4jClientError: cannot connect to server at <{base_url}>.\n"
                end
                self.node_url = root["node"].to_s
                self.batch_url = root["batch"].to_s
                self.cypher_url = root["cypher"].to_s
        end
 
-       fun service_root: Jsonable do return get("{base_url}/db/data")
+       fun service_root: Jsonable do return get(base_url / "db/data")
 
        # Is the connection with the Neo4j server ok?
        fun is_ok: Bool do return service_root isa JsonObject
@@ -246,23 +246,76 @@ class Neo4jClient
        end
 
        # Retrieve all nodes with specified `lbl`
+       #
+       #     var client = new Neo4jClient("http://localhost:7474")
+       #     #
+       #     var andres = new NeoNode
+       #     andres.labels.add_all(["Human", "Male"])
+       #     client.save_node(andres)
+       #     var kate = new NeoNode
+       #     kate.labels.add_all(["Human", "Female"])
+       #     client.save_node(kate)
+       #     #
+       #     var nodes = client.nodes_with_label("Human")
+       #     assert nodes.has(andres)
+       #     assert nodes.has(kate)
        fun nodes_with_label(lbl: String): Array[NeoNode] do
-               var res = get("{base_url}/db/data/label/{lbl}/nodes")
+               var res = get(base_url / "db/data/label/{lbl.to_percent_encoding}/nodes")
                var nodes = new Array[NeoNode]
-               var batch = new NeoBatch(self)
-               for obj in res.as(JsonArray) do
-                       var node = new NeoNode.from_json(self, obj.as(JsonObject))
-                       batch.load_node(node)
+               for json in res.as(JsonArray) do
+                       var obj = json.as(JsonObject)
+                       var node = load_node(obj["self"].to_s)
+                       node.internal_properties = obj["data"].as(JsonObject)
+                       nodes.add node
+               end
+               return nodes
+       end
+
+       # Retrieve nodes belonging to all the specified `labels`.
+       #
+       #     var client = new Neo4jClient("http://localhost:7474")
+       #     #
+       #     var andres = new NeoNode
+       #     andres.labels.add_all(["Human", "Male"])
+       #     client.save_node(andres)
+       #     var kate = new NeoNode
+       #     kate.labels.add_all(["Human", "Female"])
+       #     client.save_node(kate)
+       #     #
+       #     var nodes = client.nodes_with_labels(["Human", "Male"])
+       #     assert nodes.has(andres)
+       #     assert not nodes.has(kate)
+       fun nodes_with_labels(labels: Array[String]): Array[NeoNode] do
+               assert not labels.is_empty
+
+               # Build the query.
+               var buffer = new RopeBuffer
+               buffer.append "match n where \{label_0\} in labels(n)"
+               for i in [1..labels.length[ do
+                       buffer.append " and \{label_{i}\} in labels(n)"
+               end
+               buffer.append " return n"
+               var query = new CypherQuery.from_string(buffer.write_to_string)
+               for i in [0..labels.length[ do
+                       query.params["label_{i}"] = labels[i]
+               end
+
+               # Retrieve the answer.
+               var res = cypher(query)
+               var nodes = new Array[NeoNode]
+               for json in res.as(JsonObject)["data"].as(JsonArray) do
+                       var obj = json.as(JsonArray).first.as(JsonObject)
+                       var node = load_node(obj["self"].to_s)
+                       node.internal_properties = obj["data"].as(JsonObject)
                        nodes.add node
                end
-               batch.execute
                return nodes
        end
 
        # Perform a `CypherQuery`
        # see: CypherQuery
        fun cypher(query: CypherQuery): Jsonable do
-               return post("{cypher_url}", query.to_json)
+               return post("{cypher_url}", query.to_rest)
        end
 
        # GET JSON data from `url`
@@ -298,29 +351,31 @@ class Neo4jClient
        # Parse the cURL `response` as a JSON string
        private fun parse_response(response: CurlResponse): Jsonable do
                if response isa CurlResponseSuccess then
-                       if response.body_str.is_empty then
+                       var str = response.body_str
+                       if str.is_empty then return new JsonObject
+                       var res = str.parse_json
+                       if res isa JsonParseError then
+                               var e = new NeoError(res.to_s, "JsonParseError")
+                               e.cause = res
+                               return e
+                       end
+                       if res == null then
+                               # empty response wrap it in empty object
                                return new JsonObject
-                       else
-                               var str = response.body_str
-                               var res = str.to_jsonable
-                               if res == null then
-                                       # empty response wrap it in empty object
-                                       return new JsonObject
-                               else if res isa JsonObject and res.has_key("exception") then
-                                       var error = "Neo4jError::{res["exception"] or else "null"}"
-                                       var msg = ""
-                                       if res.has_key("message") then
-                                               msg = res["message"].to_s
-                                       end
-                                       return new JsonError(error, msg.to_json)
-                               else
-                                       return res
+                       else if res isa JsonObject and res.has_key("exception") then
+                               var error = "Neo4jError::{res["exception"] or else "null"}"
+                               var msg = ""
+                               if res.has_key("message") then
+                                       msg = res["message"].to_s
                                end
+                               return new NeoError(msg, error)
+                       else
+                               return res
                        end
                else if response isa CurlResponseFailed then
-                       return new JsonError("Curl error", "{response.error_msg} ({response.error_code})")
+                       return new NeoError("{response.error_msg} ({response.error_code})", "CurlError")
                else
-                       return new JsonError("Curl error", "Unexpected response '{response}'")
+                       return new NeoError("Unexpected response \"{response}\".", "CurlError")
                end
        end
 end
@@ -345,15 +400,11 @@ end
 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
 class CypherQuery
        # Query string to perform
-       private var query: String
+       private var query: String = ""
 
        # `params` to embed in the query like in prepared statements
        var params = new JsonObject
 
-       init do
-               self.query = ""
-       end
-
        # init the query from a query string
        init from_string(query: String) do
                self.query = query
@@ -400,8 +451,8 @@ class CypherQuery
                return self
        end
 
-       # Translate the query to JSON
-       fun to_json: JsonObject do
+       # Translate the query to the body of a corresponding Neo4j REST request.
+       fun to_rest: JsonObject do
                var obj = new JsonObject
                obj["query"] = query
                if not params.is_empty then
@@ -410,7 +461,7 @@ class CypherQuery
                return obj
        end
 
-       redef fun to_s do return to_json.to_s
+       redef fun to_s do return to_rest.to_s
 end
 
 # The fundamental units that form a graph are nodes and relationships.
@@ -449,10 +500,10 @@ end
 #     assert node["name"] == "Andres"  # loaded lazily from base
 abstract class NeoEntity
        # Neo4j client connector
-       private var neo: Neo4jClient
+       private var neo: Neo4jClient is noinit
 
        # Entity unique URL in Neo4j REST API
-       var url: nullable String
+       var url: nullable String = null
 
        # Temp id used in batch mode to update the entity
        private var batch_id: nullable Int = null
@@ -488,7 +539,7 @@ abstract class NeoEntity
        private var internal_properties: nullable JsonObject = null
 
        private fun load_properties: JsonObject do
-               var obj = neo.get("{url.to_s}/properties").as(JsonObject)
+               var obj = neo.get(url.to_s / "properties").as(JsonObject)
                internal_properties = obj
                return obj
        end
@@ -510,9 +561,6 @@ abstract class NeoEntity
 
        # Is the property `key` set?
        fun has_key(key: String): Bool do return properties.has_key(key)
-
-       # Translate `self` to JSON
-       fun to_json: JsonObject do return properties
 end
 
 # Nodes are used to represent entities stored in base.
@@ -559,7 +607,7 @@ class NeoNode
                var tpl = new FlatBuffer
                tpl.append "\{"
                tpl.append "labels: [{labels.join(", ")}],"
-               tpl.append "data: {to_json}"
+               tpl.append "data: {properties.to_json}"
                tpl.append "\}"
                return tpl.write_to_string
        end
@@ -576,7 +624,7 @@ class NeoNode
 
        private fun load_labels: Array[String] do
                var labels = new Array[String]
-               var res = neo.get("{url.to_s}/labels")
+               var res = neo.get(url.to_s / "labels")
                if res isa JsonArray then
                        for val in res do labels.add val.to_s
                end
@@ -591,9 +639,9 @@ class NeoNode
 
        private fun load_in_edges: List[NeoEdge] do
                var edges = new List[NeoEdge]
-               var res = neo.get("{url.to_s}/relationships/in").as(JsonArray)
+               var res = neo.get(url.to_s / "relationships/in").as(JsonArray)
                for obj in res do
-                       edges.add(new NeoEdge.from_json(neo.as(not null), obj.as(JsonObject)))
+                       edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
                end
                internal_in_edges = edges
                return edges
@@ -606,7 +654,7 @@ class NeoNode
 
        private fun load_out_edges: List[NeoEdge] do
                var edges = new List[NeoEdge]
-               var res = neo.get("{url.to_s}/relationships/out")
+               var res = neo.get(url.to_s / "relationships/out")
                for obj in res.as(JsonArray) do
                        edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
                end
@@ -673,7 +721,7 @@ class NeoEdge
 
        redef init from_neo(neo, url) do
                super
-               var obj = neo.get(url.as(not null)).as(JsonObject)
+               var obj = neo.get(url).as(JsonObject)
                self.internal_type = obj["type"].to_s
                self.internal_from_url = obj["start"].to_s
                self.internal_to_url = obj["end"].to_s
@@ -690,7 +738,7 @@ class NeoEdge
        fun from: NeoNode do return internal_from or else load_from
 
        private fun load_from: NeoNode do
-               var node = new NeoNode.from_neo(neo, internal_from_url.to_s)
+               var node = neo.load_node(internal_from_url.to_s)
                internal_from = node
                return node
        end
@@ -699,7 +747,7 @@ class NeoEdge
        fun to: NeoNode do return internal_to or else load_to
 
        private fun load_to: NeoNode do
-               var node = new NeoNode.from_neo(neo, internal_to_url.to_s)
+               var node = neo.load_node(internal_to_url.to_s)
                internal_to = node
                return node
        end
@@ -707,7 +755,8 @@ class NeoEdge
        # Get edge type
        fun rel_type: nullable String do return internal_type
 
-       redef fun to_json do
+       # Get the JSON body of a REST request that create the relationship.
+       private fun to_rest: JsonObject do
                var obj = new JsonObject
                if to.is_linked then
                        obj["to"] = to.url
@@ -768,13 +817,6 @@ class NeoBatch
 
        # Load a node in batch mode also load labels, data and edges
        fun load_node(node: NeoNode) do
-               load_node_data(node)
-               load_node_labels(node)
-               load_node_out_edges(node)
-       end
-
-       # Load data into node
-       private fun load_node_data(node: NeoNode) do
                var job = new_job(node)
                job.action = load_node_data_action
                job.method = "GET"
@@ -783,11 +825,7 @@ class NeoBatch
                else
                        job.to = "\{{node.batch_id.to_s}\}"
                end
-       end
-
-       # Load labels into node
-       private fun load_node_labels(node: NeoNode) do
-               var job = new_job(node)
+               job = new_job(node)
                job.action = load_node_labels_action
                job.method = "GET"
                if node.id != null then
@@ -797,9 +835,17 @@ class NeoBatch
                end
        end
 
-       # Load out edges into node
-       private fun load_node_out_edges(node: NeoNode) do
+       # Load in and out edges into node
+       fun load_node_edges(node: NeoNode) do
                var job = new_job(node)
+               job.action = load_node_in_edges_action
+               job.method = "GET"
+               if node.id != null then
+                       job.to = "/node/{node.id.to_s}/relationships/in"
+               else
+                       job.to = "\{{node.batch_id.to_s}\}/relationships/in"
+               end
+               job = new_job(node)
                job.action = load_node_out_edges_action
                job.method = "GET"
                if node.id != null then
@@ -809,6 +855,15 @@ class NeoBatch
                end
        end
 
+       # Create a `NeoNode` or a `NeoEdge` in batch mode.
+       fun save_entity(nentity: NeoEntity) do
+               if nentity isa NeoNode then
+                       save_node(nentity)
+               else if nentity isa NeoEdge then
+                       save_edge(nentity)
+               else abort
+       end
+
        # Create a node in batch mode also create labels and edges
        fun save_node(node: NeoNode) do
                if node.id != null or node.batch_id != null then return
@@ -825,7 +880,7 @@ class NeoBatch
                job.to = "\{{node.batch_id.to_s}\}/labels"
                job.body = new JsonArray.from(node.labels)
                # add edges
-               save_edges(node.out_edges)
+               #save_edges(node.out_edges)
        end
 
        # Create multiple nodes
@@ -849,18 +904,18 @@ class NeoBatch
                else
                        job.to = "\{{edge.from.batch_id.to_s}\}/relationships"
                end
-               job.body = edge.to_json
+               job.body = edge.to_rest
        end
 
        # Create multiple edges
        fun save_edges(edges: Collection[NeoEdge]) do for edge in edges do save_edge(edge)
 
        # Execute the batch and update local nodes
-       fun execute: List[JsonError] do
+       fun execute: List[NeoError] do
                var request = new JsonPOST(client.batch_url, client.curl)
                # request.headers["X-Stream"] = "true"
                var json_jobs = new JsonArray
-               for job in jobs.values do json_jobs.add job.to_json
+               for job in jobs.values do json_jobs.add job.to_rest
                request.data = json_jobs
                var response = request.execute
                var res = client.parse_response(response)
@@ -868,16 +923,16 @@ class NeoBatch
        end
 
        # Associate data from response in original nodes and edges
-       private fun finalize_batch(response: Jsonable): List[JsonError] do
-               var errors = new List[JsonError]
+       private fun finalize_batch(response: Jsonable): List[NeoError] do
+               var errors = new List[NeoError]
                if not response isa JsonArray then
-                       errors.add(new JsonError("Neo4jError", "Unexpected batch response format"))
+                       errors.add(new NeoError("Unexpected batch response format.", "Neo4jError"))
                        return errors
                end
                # print " {res.length} jobs executed"
                for res in response do
                        if not res isa JsonObject then
-                               errors.add(new JsonError("Neo4jError", "Unexpected job format in batch response"))
+                               errors.add(new NeoError("Unexpected job format in batch response.", "Neo4jError"))
                                continue
                        end
                        var id = res["id"].as(Int)
@@ -898,12 +953,19 @@ class NeoBatch
                                var labels = new Array[String]
                                for l in res["body"].as(JsonArray) do labels.add l.to_s
                                node.internal_labels = labels
+                       else if job.action == load_node_in_edges_action then
+                               var node = job.entity.as(NeoNode)
+                               var edges = res["body"].as(JsonArray)
+                               node.internal_in_edges = new List[NeoEdge]
+                               for edge in edges do
+                                       node.internal_in_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
+                               end
                        else if job.action == load_node_out_edges_action then
                                var node = job.entity.as(NeoNode)
                                var edges = res["body"].as(JsonArray)
                                node.internal_out_edges = new List[NeoEdge]
                                for edge in edges do
-                                       node.internal_out_edges.add new NeoEdge.from_json(client, edge.as(JsonObject))
+                                       node.internal_out_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
                                end
                        end
                end
@@ -917,7 +979,8 @@ class NeoBatch
        private fun create_edge_action: Int do return 2
        private fun load_node_data_action: Int do return 3
        private fun load_node_labels_action: Int do return 4
-       private fun load_node_out_edges_action: Int do return 5
+       private fun load_node_in_edges_action: Int do return 5
+       private fun load_node_out_edges_action: Int do return 6
 end
 
 # A job that can be executed in a `NeoBatch`
@@ -953,7 +1016,7 @@ class NeoJob
        var body: nullable Jsonable = null
 
        # JSON formated job
-       fun to_json: JsonObject do
+       fun to_rest: JsonObject do
                var job = new JsonObject
                job["id"] = id
                job["method"] = method