Merge: Ci: move services to specific hostnames
[nit.git] / lib / neo4j / neo4j.nit
index 496f70a..8c10f6e 100644 (file)
 
 # Neo4j connector through its JSON REST API using curl.
 #
-# For ease of use and testing this module provide a wrapper to the `neo4j` command:
-#
-#     # Start the Neo4j server
-#     var srv = new Neo4jServer
-#     assert srv.start_quiet
-#
 # In order to connect to Neo4j you need a connector:
 #
-#    # Create new Neo4j client
-#    var client = new Neo4jClient("http://localhost:7474")
-#    assert client.is_ok
+#     # Create new Neo4j client
+#     var client = new Neo4jClient("http://neo4j:7474")
+#     assert client.is_ok
 #
 # The fundamental units that form a graph are nodes and relationships.
 #
 # Nodes are used to represent entities stored in base:
 #
-#    # Create a disconnected node
-#    var andres = new NeoNode
-#    andres["name"] = "Andres"
-#    # Connect the node to Neo4j
-#    client.save_node(andres)
-#    assert andres.is_linked
-#    #
-#    # Create a second node
-#    var kate = new NeoNode
-#    kate["name"] = "Kate"
-#    client.save_node(kate)
-#    assert kate.is_linked
+#     # Create a disconnected node
+#     var andres = new NeoNode
+#     andres["name"] = "Andres"
+#     # Connect the node to Neo4j
+#     client.save_node(andres)
+#     assert andres.is_linked
+#
+#     # Create a second node
+#     var kate = new NeoNode
+#     kate["name"] = "Kate"
+#     client.save_node(kate)
+#     assert kate.is_linked
 #
 # Relationships between nodes are a key part of a graph database.
 # They allow for finding related data. Just like nodes, relationships can have properties.
 #
-#    # Create a relationship
-#    var loves = new NeoEdge(andres, "LOVES", kate)
-#    client.save_edge(loves)
-#    assert loves.is_linked
+#     # Create a relationship
+#     var loves = new NeoEdge(andres, "LOVES", kate)
+#     client.save_edge(loves)
+#     assert loves.is_linked
 #
 # Nodes can also be loaded fron Neo4j:
 #
-#    # Get a node from DB and explore edges
-#    var url = andres.url.to_s
-#    var from = client.load_node(url)
-#    assert from["name"].to_s == "Andres"
-#    var to = from.out_nodes("LOVES").first            # follow the first LOVES relationship
-#    assert to["name"].to_s == "Kate"
+#     # Get a node from DB and explore edges
+#     var url = andres.url.to_s
+#     var from = client.load_node(url)
+#     assert from["name"].to_s == "Andres"
+#     var to = from.out_nodes("LOVES").first           # follow the first LOVES relationship
+#     assert to["name"].to_s == "Kate"
 #
 # For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html
 module neo4j
@@ -66,40 +60,10 @@ module neo4j
 import curl_json
 import error
 
-# Handles Neo4j server start and stop command
-#
-# `neo4j` binary must be in `PATH` in order to work
-class Neo4jServer
-
-       # Start the local Neo4j server instance
-       fun start: Bool do
-               sys.system("neo4j start console")
-               return true
-       end
-
-       # Like `start` but redirect the console output to `/dev/null`
-       fun start_quiet: Bool do
-               sys.system("neo4j start console > /dev/null")
-               return true
-       end
-
-       # Stop the local Neo4j server instance
-       fun stop: Bool do
-               sys.system("neo4j stop")
-               return true
-       end
-
-       # Like `stop` but redirect the console output to `/dev/null`
-       fun stop_quiet: Bool do
-               sys.system("neo4j stop > /dev/null")
-               return true
-       end
-end
-
 # `Neo4jClient` is needed to communicate through the REST API
 #
-#    var client = new Neo4jClient("http://localhost:7474")
-#    assert client.is_ok
+#     var client = new Neo4jClient("http://neo4j:7474")
+#     assert client.is_ok
 class Neo4jClient
 
        # Neo4j REST services baseurl
@@ -111,8 +75,6 @@ class Neo4jClient
        # REST service to send cypher requests
        private var cypher_url: String
 
-       private var curl = new Curl
-
        init(base_url: String) do
                self.base_url = base_url
                var root = service_root
@@ -124,7 +86,7 @@ class Neo4jClient
                self.cypher_url = root["cypher"].to_s
        end
 
-       fun service_root: Jsonable do return get("{base_url}/db/data")
+       fun service_root: Serializable 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
@@ -142,8 +104,8 @@ class Neo4jClient
 
        # Save the node in base
        #
-       #     var client = new Neo4jClient("http://localhost:7474")
-       #     #
+       #     var client = new Neo4jClient("http://neo4j:7474")
+       #
        #     # Create a node
        #     var andres = new NeoNode
        #     andres["name"] = "Andres"
@@ -198,8 +160,8 @@ class Neo4jClient
        # Save the edge in base
        # From and to nodes will be created.
        #
-       #     var client = new Neo4jClient("http://localhost:7474")
-       #     #
+       #     var client = new Neo4jClient("http://neo4j:7474")
+       #
        #     var andres = new NeoNode
        #     var kate = new NeoNode
        #     var edge = new NeoEdge(andres, "LOVES", kate)
@@ -247,20 +209,20 @@ class Neo4jClient
 
        # Retrieve all nodes with specified `lbl`
        #
-       #     var client = new Neo4jClient("http://localhost:7474")
-       #     #
+       #     var client = new Neo4jClient("http://neo4j: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]
                for json in res.as(JsonArray) do
                        var obj = json.as(JsonObject)
@@ -273,21 +235,35 @@ class Neo4jClient
 
        # Retrieve nodes belonging to all the specified `labels`.
        #
-       #     var client = new Neo4jClient("http://localhost:7474")
-       #     #
+       #     var client = new Neo4jClient("http://neo4j: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
-               var res = cypher(new CypherQuery.from_string("MATCH (n:{labels.join(":")}) RETURN n"))
+
+               # Build the query.
+               var buffer = new Buffer
+               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)
@@ -300,61 +276,63 @@ class Neo4jClient
 
        # Perform a `CypherQuery`
        # see: CypherQuery
-       fun cypher(query: CypherQuery): Jsonable do
+       fun cypher(query: CypherQuery): Serializable do
                return post("{cypher_url}", query.to_rest)
        end
 
        # GET JSON data from `url`
-       fun get(url: String): Jsonable do
-               var request = new JsonGET(url, curl)
+       fun get(url: String): Serializable do
+               var request = new JsonGET(url)
                var response = request.execute
                return parse_response(response)
        end
 
        # POST `params` to `url`
-       fun post(url: String, params: Jsonable): Jsonable do
-               var request = new JsonPOST(url, curl)
-               request.data = params
+       fun post(url: String, params: Serializable): Serializable do
+               var request = new JsonPOST(url)
+               request.json_data = params
                var response = request.execute
                return parse_response(response)
        end
 
        # PUT `params` at `url`
-       fun put(url: String, params: Jsonable): Jsonable do
-               var request = new JsonPUT(url, curl)
-               request.data = params
+       fun put(url: String, params: Serializable): Serializable do
+               var request = new JsonPUT(url)
+               request.json_data = params
                var response = request.execute
                return parse_response(response)
        end
 
        # DELETE `url`
-       fun delete(url: String): Jsonable do
-               var request = new JsonDELETE(url, curl)
+       fun delete(url: String): Serializable do
+               var request = new JsonDELETE(url)
                var response = request.execute
                return parse_response(response)
        end
 
        # Parse the cURL `response` as a JSON string
-       private fun parse_response(response: CurlResponse): Jsonable do
+       private fun parse_response(response: CurlResponse): Serializable 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 NeoError(msg, error)
-                               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 NeoError("{response.error_msg} ({response.error_code})", "CurlError")
@@ -373,13 +351,13 @@ end
 #
 # Example:
 #
-#    var client = new Neo4jClient("http://localhost:7474")
-#    var query = new CypherQuery
-#    query.nmatch("(n)-[r:LOVES]->(m)")
-#    query.nwhere("n.name=\"Andres\"")
-#    query.nreturn("m.name")
-#    var res = client.cypher(query).as(JsonObject)
-#    assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
+#     var client = new Neo4jClient("http://neo4j:7474")
+#     var query = new CypherQuery
+#     query.nmatch("(n)-[r:LOVES]->(m)")
+#     query.nwhere("n.name=\"Andres\"")
+#     query.nreturn("m.name")
+#     var res = client.cypher(query).as(JsonObject)
+#     assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
 #
 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
 class CypherQuery
@@ -389,8 +367,6 @@ class CypherQuery
        # `params` to embed in the query like in prepared statements
        var params = new JsonObject
 
-       init do end
-
        # init the query from a query string
        init from_string(query: String) do
                self.query = query
@@ -401,6 +377,13 @@ class CypherQuery
                self.params = params
        end
 
+       # Pass the argument `value` as the parameter `key`.
+       #
+       # SEE: `set`
+       fun []=(key: String, value: nullable Serializable) do
+               params[key] = value
+       end
+
        # Add a `CREATE` statement to the query
        fun ncreate(query: String): CypherQuery do
                self.query = "{self.query}CREATE {query} "
@@ -437,6 +420,25 @@ class CypherQuery
                return self
        end
 
+       # Pass the argument `value` as the parameter `key`.
+       #
+       # Return `self`.
+       #
+       # ```
+       # var query = (new CypherQuery).
+       #               nmatch("(n)").
+       #               nwhere("n.key = \{key\}").
+       #               set("key", "foo")
+       #
+       # assert query.params["key"] == "foo"
+       # ```
+       #
+       # SEE: `[]=`
+       fun set(key: String, value: nullable Serializable): SELF do
+               self[key] = value
+               return self
+       end
+
        # Translate the query to the body of a corresponding Neo4j REST request.
        fun to_rest: JsonObject do
                var obj = new JsonObject
@@ -459,16 +461,16 @@ end
 #
 # If the entity is initialized unlinked from neo4j:
 #
-#    # Create a disconnected node
-#    var andres = new NeoNode
-#    andres["name"] = "Andres"
-#    # At this point, the node is not linked
-#    assert not andres.is_linked
+#     # Create a disconnected node
+#     var andres = new NeoNode
+#     andres["name"] = "Andres"
+#     # At this point, the node is not linked
+#     assert not andres.is_linked
 #
 # Then we can link the entity to the base:
 #
 #     # Init client
-#     var client = new Neo4jClient("http://localhost:7474")
+#     var client = new Neo4jClient("http://neo4j:7474")
 #     client.save_node(andres)
 #     # The node is now linked
 #     assert andres.is_linked
@@ -495,13 +497,13 @@ abstract class NeoEntity
        private var batch_id: nullable Int = null
 
        # Load the entity from base
-       private init from_neo(neo: Neo4jClient, url: String) do
+       private init from_neo(neo: Neo4jClient, url: String) is nosuper do
                self.neo = neo
                self.url = url
        end
 
        # Init entity from JSON representation
-       private init from_json(neo: Neo4jClient, obj: JsonObject) do
+       private init from_json(neo: Neo4jClient, obj: JsonObject) is nosuper do
                self.neo = neo
                self.url = obj["self"].to_s
                self.internal_properties = obj["data"].as(JsonObject)
@@ -525,7 +527,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
@@ -537,13 +539,13 @@ abstract class NeoEntity
        end
 
        # Get the entity property at `key`
-       fun [](key: String): nullable Jsonable do
+       fun [](key: String): nullable Serializable do
                if not properties.has_key(key) then return null
                return properties[key]
        end
 
        # Set the entity property `value` at `key`
-       fun []=(key: String, value: nullable Jsonable) do properties[key] = value
+       fun []=(key: String, value: nullable Serializable) do properties[key] = value
 
        # Is the property `key` set?
        fun has_key(key: String): Bool do return properties.has_key(key)
@@ -560,21 +562,21 @@ end
 #
 # Creating new nodes:
 #
-#    var client = new Neo4jClient("http://localhost:7474")
-#    #
-#    var andres = new NeoNode
-#    andres.labels.add "Person"
-#    andres["name"] = "Andres"
-#    andres["age"] = 22
-#    client.save_node(andres)
-#    assert andres.is_linked
+#     var client = new Neo4jClient("http://neo4j:7474")
+#
+#     var andres = new NeoNode
+#     andres.labels.add "Person"
+#     andres["name"] = "Andres"
+#     andres["age"] = 22
+#     client.save_node(andres)
+#     assert andres.is_linked
 #
 # Get nodes from Neo4j:
 #
-#    var url = andres.url.to_s
-#    var node = client.load_node(url)
-#    assert node["name"] == "Andres"
-#    assert node["age"].to_s.to_i      == 22
+#     var url = andres.url.to_s
+#     var node = client.load_node(url)
+#     assert node["name"] == "Andres"
+#     assert node["age"].to_s.to_i     == 22
 class NeoNode
        super NeoEntity
 
@@ -610,7 +612,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
@@ -625,7 +627,7 @@ 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, obj.as(JsonObject)))
                end
@@ -640,7 +642,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,23 +675,23 @@ end
 #
 # Create a relationship:
 #
-#    var client = new Neo4jClient("http://localhost:7474")
-#    # Create nodes
-#    var andres = new NeoNode
-#    andres["name"] = "Andres"
-#    var kate = new NeoNode
-#    kate["name"] = "Kate"
-#    # Create a relationship of type `LOVES`
-#    var loves = new NeoEdge(andres, "LOVES", kate)
-#    client.save_edge(loves)
-#    assert loves.is_linked
+#     var client = new Neo4jClient("http://neo4j:7474")
+#     # Create nodes
+#     var andres = new NeoNode
+#     andres["name"] = "Andres"
+#     var kate = new NeoNode
+#     kate["name"] = "Kate"
+#     # Create a relationship of type `LOVES`
+#     var loves = new NeoEdge(andres, "LOVES", kate)
+#     client.save_edge(loves)
+#     assert loves.is_linked
 #
 # Get an edge from DB:
 #
-#    var url = loves.url.to_s
-#    var edge = client.load_edge(url)
-#    assert edge.from["name"].to_s == "Andres"
-#    assert edge.to["name"].to_s == "Kate"
+#     var url = loves.url.to_s
+#     var edge = client.load_edge(url)
+#     assert edge.from["name"].to_s == "Andres"
+#     assert edge.to["name"].to_s == "Kate"
 class NeoEdge
        super NeoEntity
 
@@ -766,21 +768,21 @@ end
 #
 # Example:
 #
-#    var client = new Neo4jClient("http://localhost:7474")
-#    #
-#    var node1 = new NeoNode
-#    var node2 = new NeoNode
-#    var edge = new NeoEdge(node1, "TO", node2)
-#    #
-#    var batch = new NeoBatch(client)
-#    batch.save_node(node1)
-#    batch.save_node(node2)
-#    batch.save_edge(edge)
-#    batch.execute
-#    #
-#    assert node1.is_linked
-#    assert node2.is_linked
-#    assert edge.is_linked
+#     var client = new Neo4jClient("http://neo4j:7474")
+#
+#     var node1 = new NeoNode
+#     var node2 = new NeoNode
+#     var edge = new NeoEdge(node1, "TO", node2)
+#
+#     var batch = new NeoBatch(client)
+#     batch.save_node(node1)
+#     batch.save_node(node2)
+#     batch.save_edge(edge)
+#     batch.execute
+#
+#     assert node1.is_linked
+#     assert node2.is_linked
+#     assert edge.is_linked
 class NeoBatch
 
        # Neo4j client connector
@@ -898,18 +900,18 @@ class NeoBatch
 
        # Execute the batch and update local nodes
        fun execute: List[NeoError] do
-               var request = new JsonPOST(client.batch_url, client.curl)
+               var request = new JsonPOST(client.batch_url)
                # request.headers["X-Stream"] = "true"
                var json_jobs = new JsonArray
                for job in jobs.values do json_jobs.add job.to_rest
-               request.data = json_jobs
+               request.json_data = json_jobs
                var response = request.execute
                var res = client.parse_response(response)
                return finalize_batch(res)
        end
 
        # Associate data from response in original nodes and edges
-       private fun finalize_batch(response: Jsonable): List[NeoError] do
+       private fun finalize_batch(response: Serializable): List[NeoError] do
                var errors = new List[NeoError]
                if not response isa JsonArray then
                        errors.add(new NeoError("Unexpected batch response format.", "Neo4jError"))
@@ -973,7 +975,7 @@ end
 # This is a representation of a neo job in JSON Format
 #
 # Each job description should contain a `to` attribute, with a value relative to the data API root
-# (so http://localhost:7474/db/data/node becomes just /node), and a `method` attribute containing
+# (so http://neo4j:7474/db/data/node becomes just /node), and a `method` attribute containing
 # HTTP verb to use.
 #
 # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track
@@ -999,7 +1001,7 @@ class NeoJob
        # Job service target: `/node`, `/labels` etc...
        var to: String
        # Body to send with the job service request
-       var body: nullable Jsonable = null
+       var body: nullable Serializable = null
 
        # JSON formated job
        fun to_rest: JsonObject do
@@ -1013,4 +1015,3 @@ class NeoJob
                return job
        end
 end
-