Merge: Introduce `neo`: a model saver/loader from/to neo4j.
authorJean Privat <jean@pryen.org>
Wed, 23 Jul 2014 18:22:49 +0000 (14:22 -0400)
committerJean Privat <jean@pryen.org>
Wed, 23 Jul 2014 18:22:49 +0000 (14:22 -0400)
The `neo` module can be used to save or load a model from a Neo4j graph database.

Next pull requests will bring tools based on `neo`.

Pull-Request: #606
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

18 files changed:
lib/neo4j/neo4j.nit
src/neo.nit [new file with mode: 0644]
src/test_neo.nit [new file with mode: 0644]
tests/sav/test_neo.res [new file with mode: 0644]
tests/sav/test_neo_args1.res [new file with mode: 0644]
tests/test_neo.args [new file with mode: 0644]
tests/test_prog/README [new file with mode: 0644]
tests/test_prog/game/README [new file with mode: 0644]
tests/test_prog/game/game.nit [new file with mode: 0644]
tests/test_prog/platform/README [new file with mode: 0644]
tests/test_prog/platform/platform.nit [new file with mode: 0644]
tests/test_prog/rpg/README [new file with mode: 0644]
tests/test_prog/rpg/careers.nit [new file with mode: 0644]
tests/test_prog/rpg/character.nit [new file with mode: 0644]
tests/test_prog/rpg/combat.nit [new file with mode: 0644]
tests/test_prog/rpg/races.nit [new file with mode: 0644]
tests/test_prog/rpg/rpg.nit [new file with mode: 0644]
tests/test_prog/test_prog.nit [new file with mode: 0644]

index 6b3f0f3..41fc26b 100644 (file)
@@ -246,6 +246,19 @@ 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 nodes = new Array[NeoNode]
@@ -258,6 +271,33 @@ class Neo4jClient
                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
+               var res = cypher(new CypherQuery.from_string("MATCH (n:{labels.join(":")}) RETURN n"))
+               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
+               return nodes
+       end
+
        # Perform a `CypherQuery`
        # see: CypherQuery
        fun cypher(query: CypherQuery): Jsonable do
@@ -805,6 +845,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
diff --git a/src/neo.nit b/src/neo.nit
new file mode 100644 (file)
index 0000000..f7ab770
--- /dev/null
@@ -0,0 +1,772 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Save and load `Model` from/to Neo4j base.
+#
+# Nit models are composed by MEntities.
+# This module creates NeoNode for each MEntity found in a `Model` and save them into Neo4j database.
+#
+# see `Neo4jClient`.
+#
+# NeoNodes can also be translated back to MEntities to rebuild a Nit `Model`.
+#
+# Structure of the nit `Model` in base:
+#
+# `MProject`
+#
+# * labels: `model_name`, `MEntity`, `MProject`
+# * `(:MProject)-[:ROOT]->(:MGroup)`
+#
+# `MGroup`
+#
+# * labels: `model_name`, `MEntity`, `MGroup`
+# * `(:MGroup)-[:PROJECT]->(:MProject)`
+# * `(:MGroup)-[:PARENT]->(:MGroup)`
+#
+# `MModule`
+#
+# * labels: `model_name`, `MEntity`, `MModule`
+# * `(:MModule)-[:IMPORTS]->(:MModule)`
+# * `(:MModule)-[:INTRODUCES]->(:MClass)`
+# * `(:MModule)-[:DEFINES]->(:MClassDef)`
+#
+# `MClass`
+#
+# * labels: `model_name`, `MEntity`, `MClass`
+# * `(:MClass)-[:CLASSTYPE]->(:MClassType)`
+#
+# `MClassDef`
+#
+# * labels: `model_name`, `MEntity`, `MClassDef`
+# * `(:MClassDef)-[:BOUNDTYPE]->(:MClassType)`
+# * `(:MClassDef)-[:MCLASS]->(:MClass)`
+# * `(:MClassDef)-[:INTRODUCES]->(:MProperty)`
+# * `(:MClassDef)-[:DECLARES]->(:MPropDef)`
+#
+# `MProperty`
+#
+# * labels: `model_name`, `MEntity`, `MProperty`
+# * `(:MProperty)-[:INTRO_CLASSDEF]->(:MClassDef)`
+#
+# MProperties can also have labels `MMethod`, `MAttribute`, `MVirtualTypeProp`.
+#
+# `MPropDef`
+#
+# * labels: `model_name`, `MEntity`, `MPropDef`
+# * `(:MPropDef)-[:DEFINES]->(:MProperty)`
+#
+# MPropdefs can also have labels `MMethodDef`, `MAttributeDef`, `MVirtualTypeDef`.
+#
+# `MMethodDef` are linked to a `MSignature`:
+#
+# * `(:MMethodDef)-[:SIGNATURE]->(:MSignature)`
+#
+# `MVirtualTypeDef` are linked to a `MType` (its `bound`):
+#
+# * `(:MVirtualTypeDef)-[:BOUND]->(:MType)`
+#
+# `MType`
+#
+# * labels: `model_name`, `MEntity`, `MType`
+#
+# MTypes can also have labels `MClassType`, `MGenericType`, `MNullableType`, `MVirtualType`
+# and `MSignature`.
+#
+# `MClassType` and `MGenericType` both point to a `MClass` and have type `arguments`:
+#
+# * `(:MClassType)-[:CLASS]->(:MClass)`
+# * `(:MClassType)-[:ARGUMENT]->(:MType)`
+#
+# `MVirtualType` points to its introducing `MProperty`:
+#
+# * `(:MVirtualType)-[:PROPERTY]->(:MVirtualTypeDef)`
+#
+# `MParameterType` points to its introducing `MClass`:
+#
+# * `(:MParameterType)-[:CLASS]->(:MClass)`
+#
+# `MNullableType` points to its non-nullable `MType`:
+#
+# * `(:MNullableType)-[:TYPE]->(:MType)`
+#
+# `MSignature` can have `parameters` and a `return_mtype`:
+#
+# * `(:MSignature)-[:PARAMETER]->(:MParameter)`
+# * `(:MSignature)-[:RETURNTYPE]->(:MType)`
+#
+# `MParameter`
+#
+# * labels: `model_name`, `MEntity`, `MParameter`
+# * `(:MParameter)-[:TYPE]->(:MType)`
+module neo
+
+import model
+import neo4j
+import toolcontext
+
+# Helper class that can save and load a `Model` into a Neo4j database.
+class NeoModel
+
+       # The model name.
+       #
+       # Because we use only one Neo4j instance to store all the models,
+       # we need to mark their appartenance to a particular model and avoid loading all models.
+       #
+       # The name is used as a Neo label on each created nodes and used to load nodes from base.
+       var model_name: String
+
+       # The toolcontext used to init the `NeoModel` tool.
+       var toolcontext: ToolContext
+
+       # The Neo4j `client` used to communicate with the Neo4j instance.
+       var client: Neo4jClient
+
+       # Fill `model` using base pointed by `client`.
+       fun load(model: Model): Model do
+               toolcontext.info("Locate all mentities...", 1)
+               var nodes = client.nodes_with_label(model_name)
+
+               toolcontext.info("Preload nodes...", 1)
+               pull_all_nodes(nodes)
+               toolcontext.info("Preload edges...", 1)
+               pull_all_edges(nodes)
+
+               toolcontext.info("Build model...", 1)
+               nodes = client.nodes_with_labels([model_name, "MProject"])
+               for node in nodes do to_mproject(model, node)
+               nodes = client.nodes_with_labels([model_name, "MGroup"])
+               for node in nodes do to_mgroup(model, node)
+               nodes = client.nodes_with_labels([model_name, "MModule"])
+               for node in nodes do to_mmodule(model, node)
+               nodes = client.nodes_with_labels([model_name, "MClass"])
+               for node in nodes do to_mclass(model, node)
+               nodes = client.nodes_with_labels([model_name, "MClassDef"])
+               for node in nodes do to_mclassdef(model, node)
+               nodes = client.nodes_with_labels([model_name, "MProperty"])
+               for node in nodes do to_mproperty(model, node)
+               nodes = client.nodes_with_labels([model_name, "MPropDef"])
+               for node in nodes do to_mpropdef(model, node)
+               return model
+       end
+
+       # Save `model` in the base pointed by `client`.
+       fun save(model: Model) do
+               var nodes = collect_model_nodes(model)
+               toolcontext.info("Save {nodes.length} nodes...", 1)
+               push_all(nodes)
+               var edges = collect_model_edges(model)
+               toolcontext.info("Save {edges.length} edges...", 1)
+               push_all(edges)
+       end
+
+       # Save `neo_entities` in base using batch mode.
+       private fun push_all(neo_entities: Collection[NeoEntity]) do
+               var batch = new NeoBatch(client)
+               var len = neo_entities.length
+               var sum = 0
+               var i = 1
+               for nentity in neo_entities do
+                       batch.save_entity(nentity)
+                       if i == batch_max_size then
+                               do_batch(batch)
+                               sum += batch_max_size
+                               toolcontext.info(" {sum * 100 / len}% done", 1)
+                               batch = new NeoBatch(client)
+                               i = 1
+                       else
+                               i += 1
+                       end
+               end
+               do_batch(batch)
+       end
+
+       # Load content for all `nodes` from base.
+       #
+       # Content corresponds to properties and labels that are loaded in batch mode.
+       private fun pull_all_nodes(nodes: Collection[NeoNode]) do
+               var batch = new NeoBatch(client)
+               var len = nodes.length
+               var sum = 0
+               var i = 1
+               for node in nodes do
+                       batch.load_node(node)
+                       if i == batch_max_size then
+                               do_batch(batch)
+                               sum += batch_max_size
+                               toolcontext.info(" {sum * 100 / len}% done", 1)
+                               batch = new NeoBatch(client)
+                               i = 1
+                       else
+                               i += 1
+                       end
+               end
+               do_batch(batch)
+       end
+
+       # Load all edges from base linked to `nodes`.
+       #
+       # Edges are loaded in batch mode.
+       private fun pull_all_edges(nodes: Collection[NeoNode]) do
+               var batch = new NeoBatch(client)
+               var len = nodes.length
+               var sum = 0
+               var i = 1
+               for node in nodes do
+                       batch.load_node_edges(node)
+                       if i == batch_max_size then
+                               do_batch(batch)
+                               sum += batch_max_size
+                               toolcontext.info(" {sum * 100 / len}% done", 1)
+                               batch = new NeoBatch(client)
+                               i = 1
+                       else
+                               i += 1
+                       end
+               end
+               do_batch(batch)
+       end
+
+       # How many operation can be executed in one batch?
+       private var batch_max_size = 1000
+
+       # Execute `batch` and check for errors.
+       #
+       # Abort if `batch.execute` returns errors.
+       private fun do_batch(batch: NeoBatch) do
+               var errors = batch.execute
+               if not errors.is_empty then
+                       print errors
+                       exit(1)
+               end
+       end
+
+       # Collect all nodes from the current `model`.
+       private fun collect_model_nodes(model: Model): Collection[NeoNode] do
+               for mproject in model.mprojects do
+                       to_node(mproject)
+                       for mgroup in mproject.mgroups do to_node(mgroup)
+               end
+               return nodes.values
+       end
+
+       # Collect all edges from the current `model`.
+       #
+       # Actually collect all out_edges from all nodes.
+       private fun collect_model_edges(model: Model): Collection[NeoEdge] do
+               var edges = new HashSet[NeoEdge]
+               for node in nodes.values do edges.add_all(node.out_edges)
+               return edges
+       end
+
+       # Mentities associated to nodes.
+       private var mentities = new HashMap[NeoNode, MEntity]
+
+       # Nodes associated with MEntities.
+       private var nodes = new HashMap[MEntity, NeoNode]
+
+       # Get the `NeoNode` associated with `mentity`.
+       # `mentities` are stored locally to avoid duplication.
+       fun to_node(mentity: MEntity): NeoNode do
+               if nodes.has_key(mentity) then return nodes[mentity]
+               if mentity isa MProject then return mproject_node(mentity)
+               if mentity isa MGroup then return mgroup_node(mentity)
+               if mentity isa MModule then return mmodule_node(mentity)
+               if mentity isa MClass then return mclass_node(mentity)
+               if mentity isa MClassDef then return mclassdef_node(mentity)
+               if mentity isa MProperty then return mproperty_node(mentity)
+               if mentity isa MPropDef then return mpropdef_node(mentity)
+               if mentity isa MType then return mtype_node(mentity)
+               if mentity isa MParameter then return mparameter_node(mentity)
+               abort
+       end
+
+       # Make a new `NeoNode` based on `mentity`.
+       private fun make_node(mentity: MEntity): NeoNode do
+               var node = new NeoNode
+               nodes[mentity] = node
+               node.labels.add "MEntity"
+               node.labels.add model_name
+               node["name"] = mentity.name
+               if mentity.mdoc != null then node["mdoc"] = new JsonArray.from(mentity.mdoc.content)
+               return node
+       end
+
+       # Build a `NeoNode` representing `mproject`.
+       private fun mproject_node(mproject: MProject): NeoNode do
+               var node = make_node(mproject)
+               node.labels.add "MProject"
+               var root = mproject.root
+               if root != null then
+                       node.out_edges.add(new NeoEdge(node, "ROOT", to_node(root)))
+               end
+               return node
+       end
+
+       # Build a new `MProject` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MProject")`
+       private fun to_mproject(model: Model, node: NeoNode): MProject do
+               if mentities.has_key(node) then return mentities[node].as(MProject)
+               assert node.labels.has("MProject")
+               var mproject = new MProject(node["name"].to_s, model)
+               mentities[node] = mproject
+               set_doc(node, mproject)
+               mproject.root = to_mgroup(model, node.out_nodes("ROOT").first)
+               return mproject
+       end
+
+       # Build a `NeoNode` representing `mgroup`.
+       private fun mgroup_node(mgroup: MGroup): NeoNode do
+               var node = make_node(mgroup)
+               node.labels.add "MGroup"
+               node["full_name"] = mgroup.full_name
+               var parent = mgroup.parent
+               node.out_edges.add(new NeoEdge(node, "PROJECT", to_node(mgroup.mproject)))
+               if parent != null then
+                       node.out_edges.add(new NeoEdge(node, "PARENT", to_node(parent)))
+               end
+               for mmodule in mgroup.mmodules do
+                       node.out_edges.add(new NeoEdge(node, "DECLARES", to_node(mmodule)))
+               end
+               for subgroup in mgroup.in_nesting.direct_smallers do
+                       node.in_edges.add(new NeoEdge(node, "NESTS", to_node(subgroup)))
+               end
+               return node
+       end
+
+       # Build a new `MGroup` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MGroup")`
+       private fun to_mgroup(model: Model, node: NeoNode): MGroup do
+               if mentities.has_key(node) then return mentities[node].as(MGroup)
+               assert node.labels.has("MGroup")
+               var mproject = to_mproject(model, node.out_nodes("PROJECT").first)
+               var parent: nullable MGroup = null
+               var out = node.out_nodes("PARENT")
+               if not out.is_empty then
+                       parent = to_mgroup(model, out.first)
+               end
+               var mgroup = new MGroup(node["name"].to_s, mproject, parent)
+               mentities[node] = mgroup
+               set_doc(node, mgroup)
+               return mgroup
+       end
+
+       # Build a `NeoNode` representing `mmodule`.
+       private fun mmodule_node(mmodule: MModule): NeoNode do
+               var node = make_node(mmodule)
+               node.labels.add "MModule"
+               node["full_name"] = mmodule.full_name
+               node["location"] = mmodule.location.to_s
+               var mgroup = mmodule.mgroup
+               for parent in mmodule.in_importation.direct_greaters do
+                       node.out_edges.add(new NeoEdge(node, "IMPORTS", to_node(parent)))
+               end
+               for mclass in mmodule.intro_mclasses do
+                       node.out_edges.add(new NeoEdge(node, "INTRODUCES", to_node(mclass)))
+               end
+               for mclassdef in mmodule.mclassdefs do
+                       node.out_edges.add(new NeoEdge(node, "DEFINES", to_node(mclassdef)))
+               end
+               return node
+       end
+
+       # Build a new `MModule` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MModule")`
+       private fun to_mmodule(model: Model, node: NeoNode): MModule do
+               if mentities.has_key(node) then return mentities[node].as(MModule)
+               assert node.labels.has("MModule")
+               var ins = node.in_nodes("DECLARES")
+               var mgroup: nullable MGroup = null
+               if not ins.is_empty then
+                       mgroup = to_mgroup(model, ins.first)
+               end
+               var name = node["name"].to_s
+               var location = to_location(node["location"].to_s)
+               var mmodule = new MModule(model, mgroup, name, location)
+               mentities[node] = mmodule
+               set_doc(node, mmodule)
+               var imported_mmodules = new Array[MModule]
+               for smod in node.out_nodes("IMPORTS") do
+                       imported_mmodules.add to_mmodule(model, smod)
+               end
+               mmodule.set_imported_mmodules(imported_mmodules)
+               return mmodule
+       end
+
+       # Build a `NeoNode` representing `mclass`.
+       private fun mclass_node(mclass: MClass): NeoNode do
+               var node = make_node(mclass)
+               node.labels.add "MClass"
+               node["arity"] = mclass.arity
+               node["full_name"] = mclass.full_name
+               node["kind"] = mclass.kind.to_s
+               node["visibility"] = mclass.visibility.to_s
+               node.out_edges.add(new NeoEdge(node, "CLASSTYPE", to_node(mclass.mclass_type)))
+               return node
+       end
+
+       # Build a new `MClass` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MClass")`
+       private fun to_mclass(model: Model, node: NeoNode): MClass do
+               if mentities.has_key(node) then return mentities[node].as(MClass)
+               assert node.labels.has("MClass")
+               var mmodule = to_mmodule(model, node.in_nodes("INTRODUCES").first)
+               var name = node["name"].to_s
+               var arity = node["arity"].to_s.to_i
+               var kind = to_kind(node["kind"].to_s)
+               var visibility = to_visibility(node["visibility"].to_s)
+               var mclass = new MClass(mmodule, name, arity, kind, visibility)
+               mentities[node] = mclass
+               set_doc(node, mclass)
+               return mclass
+       end
+
+       # Build a `NeoNode` representing `mclassdef`.
+       private fun mclassdef_node(mclassdef: MClassDef): NeoNode do
+               var node = make_node(mclassdef)
+               node.labels.add "MClassDef"
+               node["is_intro"] = mclassdef.is_intro
+               node["location"] = mclassdef.location.to_s
+               if not mclassdef.parameter_names.is_empty then
+                       node["parameter_names"] = new JsonArray.from(mclassdef.parameter_names)
+               end
+               node.out_edges.add(new NeoEdge(node, "BOUNDTYPE", to_node(mclassdef.bound_mtype)))
+               node.out_edges.add(new NeoEdge(node, "MCLASS", to_node(mclassdef.mclass)))
+               for mproperty in mclassdef.intro_mproperties do
+                       node.out_edges.add(new NeoEdge(node, "INTRODUCES", to_node(mproperty)))
+               end
+               for mpropdef in mclassdef.mpropdefs do
+                       node.out_edges.add(new NeoEdge(node, "DECLARES", to_node(mpropdef)))
+               end
+               for sup in mclassdef.supertypes do
+                       node.out_edges.add(new NeoEdge(node, "INHERITS", to_node(sup)))
+               end
+               return node
+       end
+
+       # Build a new `MClassDef` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MClassDef")`
+       private fun to_mclassdef(model: Model, node: NeoNode): MClassDef do
+               if mentities.has_key(node) then return mentities[node].as(MClassDef)
+               assert node.labels.has("MClassDef")
+               var mmodule = to_mmodule(model, node.in_nodes("DEFINES").first)
+               var mtype = to_mtype(model, node.out_nodes("BOUNDTYPE").first).as(MClassType)
+               var location = to_location(node["location"].to_s)
+               var parameter_names = new Array[String]
+               if node.has_key("parameter_names") then
+                       for e in node["parameter_names"].as(JsonArray) do
+                               parameter_names.add e.to_s
+                       end
+               end
+               var mclassdef = new MClassDef(mmodule, mtype, location, parameter_names)
+               mentities[node] = mclassdef
+               set_doc(node, mclassdef)
+               var supertypes = new Array[MClassType]
+               for sup in node.out_nodes("INHERITS") do
+                       supertypes.add to_mtype(model, sup).as(MClassType)
+               end
+               mclassdef.set_supertypes(supertypes)
+               mclassdef.add_in_hierarchy
+               return mclassdef
+       end
+
+       # Build a `NeoNode` representing `mproperty`.
+       private fun mproperty_node(mproperty: MProperty): NeoNode do
+               var node = make_node(mproperty)
+               node.labels.add "MProperty"
+               node["full_name"] = mproperty.full_name
+               node["visibility"] = mproperty.visibility.to_s
+               if mproperty isa MMethod then
+                       node.labels.add "MMethod"
+                       node["is_init"] = mproperty.is_init
+               else if mproperty isa MAttribute then
+                       node.labels.add "MAttribute"
+               else if mproperty isa MVirtualTypeProp then
+                       node.labels.add "MVirtualTypeProp"
+               end
+               node.out_edges.add(new NeoEdge(node, "INTRO_CLASSDEF", to_node(mproperty.intro_mclassdef)))
+               return node
+       end
+
+       # Build a new `MProperty` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MProperty")`
+       private fun to_mproperty(model: Model, node: NeoNode): MProperty do
+               if mentities.has_key(node) then return mentities[node].as(MProperty)
+               assert node.labels.has("MProperty")
+               var intro_mclassdef = to_mclassdef(model, node.out_nodes("INTRO_CLASSDEF").first)
+               var name = node["name"].to_s
+               var visibility = to_visibility(node["visibility"].to_s)
+               var mprop: nullable MProperty = null
+               if node.labels.has("MMethod") then
+                       mprop = new MMethod(intro_mclassdef, name, visibility)
+                       mprop.is_init = node["is_init"].as(Bool)
+               else if node.labels.has("MAttribute") then
+                       mprop = new MAttribute(intro_mclassdef, name, visibility)
+               else if node.labels.has("MVirtualTypeProp") then
+                       mprop = new MVirtualTypeProp(intro_mclassdef, name, visibility)
+               end
+               if mprop == null then
+                       print "not yet implemented to_mproperty for {node.labels.join(",")}"
+                       abort
+               end
+               mentities[node] = mprop
+               set_doc(node, mprop)
+               for npropdef in node.in_nodes("DEFINES") do
+                       var mpropdef = to_mpropdef(model, npropdef)
+                       if npropdef["is_intro"].as(Bool) then
+                               mprop.mpropdefs.unshift mpropdef
+                       else
+                               mprop.mpropdefs.add mpropdef
+                       end
+               end
+               return mprop
+       end
+
+       # Build a `NeoNode` representing `mpropdef`.
+       private fun mpropdef_node(mpropdef: MPropDef): NeoNode do
+               var node = make_node(mpropdef)
+               node.labels.add "MPropDef"
+               node["is_intro"] = mpropdef.is_intro
+               node["location"] = mpropdef.location.to_s
+               node.out_edges.add(new NeoEdge(node, "DEFINES", to_node(mpropdef.mproperty)))
+               if mpropdef isa MMethodDef then
+                       node.labels.add "MMethodDef"
+                       node["is_abstract"] = mpropdef.is_abstract
+                       node["is_intern"] = mpropdef.is_intern
+                       node["is_extern"] = mpropdef.is_extern
+                       var msignature = mpropdef.msignature
+                       if msignature != null then
+                               node.out_edges.add(new NeoEdge(node, "SIGNATURE", to_node(msignature)))
+                       end
+               else if mpropdef isa MAttributeDef then
+                       node.labels.add "MAttributeDef"
+               else if mpropdef isa MVirtualTypeDef then
+                       node.labels.add "MVirtualTypeDef"
+                       var bound = mpropdef.bound
+                       if bound != null then
+                               node.out_edges.add(new NeoEdge(node, "BOUND", to_node(bound)))
+                       end
+               end
+               return node
+       end
+
+       # Build a new `MPropDef` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MPropDef")`
+       private fun to_mpropdef(model: Model, node: NeoNode): MPropDef do
+               if mentities.has_key(node) then return mentities[node].as(MPropDef)
+               assert node.labels.has("MPropDef")
+               var mclassdef = to_mclassdef(model, node.in_nodes("DECLARES").first)
+               var mproperty = to_mproperty(model, node.out_nodes("DEFINES").first)
+               var location = to_location(node["location"].to_s)
+               var mpropdef: nullable MPropDef = null
+               if node.labels.has("MMethodDef") then
+                       mpropdef = new MMethodDef(mclassdef, mproperty.as(MMethod), location)
+                       mpropdef.is_abstract = node["is_abstract"].as(Bool)
+                       mpropdef.is_intern = node["is_intern"].as(Bool)
+                       mpropdef.is_extern = node["is_extern"].as(Bool)
+                       mentities[node] = mpropdef
+                       mpropdef.msignature = to_mtype(model, node.out_nodes("SIGNATURE").first).as(MSignature)
+               else if node.labels.has("MAttributeDef") then
+                       mpropdef = new MAttributeDef(mclassdef, mproperty.as(MAttribute), location)
+                       mentities[node] = mpropdef
+               else if node.labels.has("MVirtualTypeDef") then
+                       mpropdef = new MVirtualTypeDef(mclassdef, mproperty.as(MVirtualTypeProp), location)
+                       mentities[node] = mpropdef
+                       var bound = node.out_nodes("BOUND")
+                       if not bound.is_empty then mpropdef.bound = to_mtype(model, bound.first)
+               end
+               if mpropdef == null then
+                       print "not yet implemented to_mpropdef for {node.labels.join(",")}"
+                       abort
+               end
+               set_doc(node, mpropdef)
+               return mpropdef
+       end
+
+       # Build a `NeoNode` representing `mtype`.
+       private fun mtype_node(mtype: MType): NeoNode do
+               var node = make_node(mtype)
+               node.labels.add "MType"
+               if mtype isa MClassType then
+                       node.labels.add "MClassType"
+                       node.out_edges.add(new NeoEdge(node, "CLASS", to_node(mtype.mclass)))
+                       for arg in mtype.arguments do
+                               node.out_edges.add(new NeoEdge(node, "ARGUMENT", to_node(arg)))
+                       end
+                       if mtype isa MGenericType then
+                               node.labels.add "MGenericType"
+                       end
+               else if mtype isa MVirtualType then
+                       node.labels.add "MVirtualType"
+                       node.out_edges.add(new NeoEdge(node, "PROPERTY", to_node(mtype.mproperty)))
+               else if mtype isa MParameterType then
+                       node.labels.add "MParameterType"
+                       node["rank"] = mtype.rank
+                       node.out_edges.add(new NeoEdge(node, "CLASS", to_node(mtype.mclass)))
+               else if mtype isa MNullableType then
+                       node.labels.add "MNullableType"
+                       node.out_edges.add(new NeoEdge(node, "TYPE", to_node(mtype.mtype)))
+               else if mtype isa MSignature then
+                       node.labels.add "MSignature"
+                       for mparameter in mtype.mparameters do
+                               node.out_edges.add(new NeoEdge(node, "PARAMETER", to_node(mparameter)))
+                       end
+                       var return_mtype = mtype.return_mtype
+                       if return_mtype != null then
+                               node.out_edges.add(new NeoEdge(node, "RETURNTYPE", to_node(return_mtype)))
+                       end
+               end
+               return node
+       end
+
+       # Build a new `MType` from a `node`.
+       #
+       # REQUIRE `node.labels.has("MType")`
+       private fun to_mtype(model: Model, node: NeoNode): MType do
+               if mentities.has_key(node) then return mentities[node].as(MType)
+               assert node.labels.has("MType")
+               if node.labels.has("MClassType") then
+                       var mclass = to_mclass(model, node.out_nodes("CLASS").first)
+                       var args = new Array[MType]
+                       for narg in node.out_nodes("ARGUMENT") do
+                               args.add to_mtype(model, narg)
+                       end
+                       var mtype = mclass.get_mtype(args)
+                       mentities[node] = mtype
+                       return mtype
+               else if node.labels.has("MParameterType") then
+                       var mclass = to_mclass(model, node.out_nodes("CLASS").first)
+                       var rank = node["rank"].to_s.to_i
+                       var mtype = new MParameterType(mclass, rank)
+                       mentities[node] = mtype
+                       return  mtype
+               else if node.labels.has("MNullableType") then
+                       var intype = to_mtype(model, node.out_nodes("TYPE").first)
+                       var mtype = new MNullableType(intype)
+                       mentities[node] = mtype
+                       return mtype
+               else if node.labels.has("MVirtualType") then
+                       var mproperty = to_mproperty(model, node.out_nodes("PROPERTY").first)
+                       var mtype = new MVirtualType(mproperty)
+                       mentities[node] = mtype
+                       return mtype
+               else if node.labels.has("MSignature") then
+                       var mparameters = new Array[MParameter]
+                       for pnode in node.out_nodes("PARAMETER") do
+                               mparameters.add to_mparameter(model, pnode)
+                       end
+                       var return_mtype: nullable MType = null
+                       var ret_nodes = node.out_nodes("RETURNTYPE")
+                       if not ret_nodes.is_empty then
+                               return_mtype = to_mtype(model, ret_nodes.first)
+                       end
+                       var mtype = new MSignature(mparameters, return_mtype)
+                       mentities[node] = mtype
+                       return mtype
+               end
+               print "not yet implemented to_mtype for {node.labels.join(",")}"
+               abort
+       end
+
+       # Build a `NeoNode` representing `mparameter`.
+       private fun mparameter_node(mparameter: MParameter): NeoNode do
+               var node = make_node(mparameter)
+               node.labels.add "MParameter"
+               node["name"] = mparameter.name
+               node["is_vararg"] = mparameter.is_vararg
+               node.out_edges.add(new NeoEdge(node, "TYPE", to_node(mparameter.mtype)))
+               return node
+       end
+
+       # Build a new `MParameter` from `node`.
+       #
+       # REQUIRE `node.labels.has("MParameter")`
+       private fun to_mparameter(model: Model, node: NeoNode): MParameter do
+               if mentities.has_key(node) then return mentities[node].as(MParameter)
+               assert node.labels.has("MParameter")
+               var name = node["name"].to_s
+               var mtype = to_mtype(model, node.out_nodes("TYPE").first)
+               var is_vararg = node["is_vararg"].as(Bool)
+               var mparameter = new MParameter(name, mtype, is_vararg)
+               mentities[node] = mparameter
+               return mparameter
+       end
+
+       # Get a `Location` from its string representation.
+       private fun to_location(loc: String): Location do
+               #TODO filepath
+               var parts = loc.split_with(":")
+               var file = new SourceFile.from_string(parts[0], "")
+               var pos = parts[1].split_with("--")
+               var pos1 = pos[0].split_with(",")
+               var pos2 = pos[1].split_with(",")
+               var line_s = pos1[0].to_i
+               var line_e = pos2[0].to_i
+               var column_s = pos1[1].to_i
+               var column_e = 0
+               if pos2.length == 2 then pos2[1].to_i
+               return new Location(file, line_s, line_e, column_s, column_e)
+       end
+
+       # Get a `MVisibility` from its string representation.
+       private fun to_visibility(vis: String): MVisibility do
+               if vis == intrude_visibility.to_s then
+                       return intrude_visibility
+               else if vis == public_visibility.to_s then
+                       return public_visibility
+               else if vis == protected_visibility.to_s then
+                       return protected_visibility
+               else if vis == private_visibility.to_s then
+                       return private_visibility
+               else
+                       return none_visibility
+               end
+       end
+
+       # Get a `MKind` from its string representation.
+       private fun to_kind(kind: String): MClassKind do
+               if kind == abstract_kind.to_s then
+                       return abstract_kind
+               else if kind == concrete_kind.to_s then
+                       return concrete_kind
+               else if kind == interface_kind.to_s then
+                       return interface_kind
+               else if kind == enum_kind.to_s then
+                       return enum_kind
+               else if kind == extern_kind.to_s then
+                       return extern_kind
+               end
+               abort
+       end
+
+       # Extract the `MDoc` from `node` and link it to `mentity`.
+       private fun set_doc(node: NeoNode, mentity: MEntity) do
+               if node.has_key("mdoc") then
+                       var lines = new Array[String]
+                       for e in node["mdoc"].as(JsonArray) do
+                               lines.add e.to_s#.replace("\n", "\\n")
+                       end
+                       var mdoc = new MDoc
+                       mdoc.content.add_all(lines)
+                       mdoc.original_mentity = mentity
+                       mentity.mdoc = mdoc
+               end
+       end
+end
diff --git a/src/test_neo.nit b/src/test_neo.nit
new file mode 100644 (file)
index 0000000..37adf04
--- /dev/null
@@ -0,0 +1,90 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test for neo model saving and loading.
+module test_neo
+
+import neo
+import model_utils
+import frontend
+
+var test_name = "test_{get_time.to_s}"
+
+# init tool
+var toolcontext = new ToolContext
+toolcontext.tooldescription = "Usage: neo_saver host port files..."
+toolcontext.process_options(args)
+var arguments = toolcontext.option_context.rest
+
+if arguments.length < 3 then
+       toolcontext.usage
+       exit 0
+end
+
+var host = arguments.shift
+var port = arguments.shift
+var url = "http://{host}:{port}"
+
+# parse model
+toolcontext.info("Parse files...", 1)
+var org_model = new Model
+var modelbuilder = new ModelBuilder(org_model, toolcontext)
+modelbuilder.parse(arguments)
+modelbuilder.run_phases
+
+toolcontext.info("Open connection to neo4j on {url} for saving...", 1)
+var save_client = new Neo4jClient(url)
+var save_model = new NeoModel(test_name, toolcontext, save_client)
+save_model.save(org_model)
+
+toolcontext.info("Open connection to neo4j on {url} for reading...", 1)
+var read_client = new Neo4jClient(url)
+var neo_model = new Model
+var read_model = new NeoModel(test_name, toolcontext, read_client)
+read_model.load(neo_model)
+
+# Compare model
+var sorter = new MEntityNameSorter
+
+print "mprojects:"
+var org_mprojects = org_model.mprojects.to_a
+sorter.sort org_mprojects
+print org_mprojects.join(" ")
+var neo_mprojects = neo_model.mprojects.to_a
+sorter.sort neo_mprojects
+print neo_mprojects.join(" ")
+
+print "mmodules:"
+var org_mmodules = org_model.mmodules.to_a
+sorter.sort org_mmodules
+print org_mmodules.join(" ")
+var neo_mmodules = neo_model.mmodules.to_a
+sorter.sort neo_mmodules
+print neo_mmodules.join(" ")
+
+print "mclasses:"
+var org_mclasses = org_model.mclasses.to_a
+sorter.sort org_mclasses
+print org_mclasses.join(" ")
+var neo_mclasses = neo_model.mclasses.to_a
+sorter.sort neo_mclasses
+print neo_mclasses.join(" ")
+
+print "mproperties:"
+var org_mproperties = org_model.mproperties.to_a
+sorter.sort org_mproperties
+print org_mproperties.join(" ")
+var neo_mproperties = neo_model.mproperties.to_a
+sorter.sort neo_mproperties
+print neo_mproperties.join(" ")
diff --git a/tests/sav/test_neo.res b/tests/sav/test_neo.res
new file mode 100644 (file)
index 0000000..501e8ca
--- /dev/null
@@ -0,0 +1,2 @@
+Usage: neo_saver host port files...
+Use --help for help
diff --git a/tests/sav/test_neo_args1.res b/tests/sav/test_neo_args1.res
new file mode 100644 (file)
index 0000000..8b3a417
--- /dev/null
@@ -0,0 +1,12 @@
+mprojects:
+test_prog
+test_prog
+mmodules:
+careers character combat game platform races rpg test_prog
+careers character combat game platform races rpg test_prog
+mclasses:
+Alcoholic Bool Career Character Combatable Dwarf Elf Float Game Human Int List Magician Object Race Starter String Sys Warrior Weapon
+Alcoholic Bool Career Character Combatable Dwarf Elf Float Game Human Int List Magician Object Race Starter String Sys Warrior Weapon
+mproperties:
+!= * * + + - - / / == > > OTHER _age _base_endurance _base_intelligence _base_strength _career _endurance_bonus _health _intelligence_bonus _name _race _sex _strength_bonus age age= attack base_endurance base_endurance= base_intelligence base_intelligence= base_strength base_strength= career career= computer_characters defend direct_attack dps endurance_bonus endurance_bonus= health health= hit_points init init init init init init init init init init intelligence_bonus intelligence_bonus= is_dead main max_health name name= pause_game player_characters quit race race= sex sex= start start_game stop_game strength_bonus strength_bonus= to_f total_endurance total_intelligence total_strengh unary -
+!= * * + + - - / / == > > OTHER _age _base_endurance _base_intelligence _base_strength _career _endurance_bonus _health _intelligence_bonus _name _race _sex _strength_bonus age age= attack base_endurance base_endurance= base_intelligence base_intelligence= base_strength base_strength= career career= computer_characters defend direct_attack dps endurance_bonus endurance_bonus= health health= hit_points init init init init init init init init init init intelligence_bonus intelligence_bonus= is_dead main max_health name name= pause_game player_characters quit race race= sex sex= start start_game stop_game strength_bonus strength_bonus= to_f total_endurance total_intelligence total_strengh unary -
diff --git a/tests/test_neo.args b/tests/test_neo.args
new file mode 100644 (file)
index 0000000..a1ee088
--- /dev/null
@@ -0,0 +1 @@
+localhost 7474 test_prog/test_prog.nit
diff --git a/tests/test_prog/README b/tests/test_prog/README
new file mode 100644 (file)
index 0000000..e71ce81
--- /dev/null
@@ -0,0 +1,9 @@
+Test program for model tools.
+
+This program creates a fake model that can be used to test tools like:
+
+* `nitdoc`
+* `nitmetrics`
+* `nitx`
+* or others `modelbuilder`.
+
diff --git a/tests/test_prog/game/README b/tests/test_prog/game/README
new file mode 100644 (file)
index 0000000..8addc65
--- /dev/null
@@ -0,0 +1,2 @@
+Gaming group
+
diff --git a/tests/test_prog/game/game.nit b/tests/test_prog/game/game.nit
new file mode 100644 (file)
index 0000000..e7a7348
--- /dev/null
@@ -0,0 +1,46 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A game abstraction for RPG.
+module game
+
+import rpg
+
+# This is the interface you have to implement to use ure gaming platform.
+#
+# see http://our.platform.com
+interface Game
+
+       # Characters played by human players.
+       fun player_characters: List[Character] is abstract
+
+       # Characters players by computer.
+       fun computer_characters: List[Character] is abstract
+
+       # Start the game.
+       #
+       # You have to implement that method!
+       fun start_game is abstract
+
+       # Pause the game.
+       #
+       # You have to implement that method!
+       fun pause_game is abstract
+
+       # Stop the game.
+       #
+       # You have to implement that method!
+       fun stop_game is abstract
+end
+
diff --git a/tests/test_prog/platform/README b/tests/test_prog/platform/README
new file mode 100644 (file)
index 0000000..1958baa
--- /dev/null
@@ -0,0 +1,2 @@
+Fictive Crappy Platform.
+
diff --git a/tests/test_prog/platform/platform.nit b/tests/test_prog/platform/platform.nit
new file mode 100644 (file)
index 0000000..d474ea0
--- /dev/null
@@ -0,0 +1,59 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Declares base types allowed on the platform.
+module platform
+
+import end
+
+# Root of everything.
+class Object
+       # Used for comparisons.
+       type OTHER: nullable Object
+
+       # Is `other` equqls to `self`?
+       fun ==(other: OTHER): Bool is intern
+
+       # Is `other` different from `self`?
+       fun !=(other: OTHER): Bool do return not self == other
+end
+
+# Some services about Integers.
+class Int
+       fun -: Int is intern
+       fun +(i: Int): Int is intern
+       fun -(i: Int): Int is intern
+       fun *(i: Int): Int is intern
+       fun /(i: Int): Int is intern
+       fun >(i: Int): Bool is intern
+       fun to_f: Float is intern
+end
+
+# Some services about Floats.
+class Float
+       fun +(f: Float): Float is intern
+       fun -(f: Float): Float is intern
+       fun *(f: Float): Float is intern
+       fun /(f: Float): Float is intern
+       fun >(f: Float): Bool is intern
+end
+
+# Booleans, `true` or `false`.
+class Bool end
+
+# Strings (there is no chars...).
+class String end
+
+# List of things.
+class List[E] end
diff --git a/tests/test_prog/rpg/README b/tests/test_prog/rpg/README
new file mode 100644 (file)
index 0000000..683bccc
--- /dev/null
@@ -0,0 +1,2 @@
+Role Playing Game group
+
diff --git a/tests/test_prog/rpg/careers.nit b/tests/test_prog/rpg/careers.nit
new file mode 100644 (file)
index 0000000..deef5c9
--- /dev/null
@@ -0,0 +1,70 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Careers of the game.
+#
+# All characters can have a `Career`.
+# A character can also quit its current career and start a new one.
+#
+# Available careers:
+#
+#  * `Warrior`
+#  * `Magician`
+#  * `Alcoholic`
+module careers
+
+import platform
+
+# A `Career` gives a characteristic bonus or malus to the character.
+abstract class Career
+       var strength_bonus: Int
+       var endurance_bonus: Int
+       var intelligence_bonus: Int
+
+       init do end
+end
+
+# Warriors are good for fighting.
+class Warrior
+       super Career
+
+       init do
+               self.strength_bonus = 10
+               self.endurance_bonus = 10
+               self.intelligence_bonus = 0
+       end
+end
+
+# Magicians know magic and how to use it.
+class Magician
+       super Career
+
+       init do
+               self.strength_bonus = -5
+               self.endurance_bonus = 0
+               self.intelligence_bonus = 20
+       end
+end
+
+# Alcoholics are good to nothing escept taking punches.
+class Alcoholic
+       super Career
+
+       init do
+               self.strength_bonus = -20
+               self.endurance_bonus = 20
+               self.intelligence_bonus = -40
+       end
+end
+
diff --git a/tests/test_prog/rpg/character.nit b/tests/test_prog/rpg/character.nit
new file mode 100644 (file)
index 0000000..a494875
--- /dev/null
@@ -0,0 +1,69 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Characters are playable entity in the world.
+module character
+
+import races
+import careers
+
+# Characters can be played by both the human or the machine.
+class Character
+
+       # The `Race` of the character.
+       var race: Race
+
+       # The current `Career` of the character.
+       # Returns `null` if character is unemployed.
+       var career: nullable Career writable = null
+
+       fun quit do
+               career = null
+       end
+
+       var name: String
+       var age: Int
+       var sex: Bool
+
+       # The actual strength of the character.
+       #
+       # Returns `race.base_strength + career.strength_bonus` or just `race.base_strength` is unemployed.
+       fun total_strengh: Int do
+               if career != null then return race.base_strength + career.strength_bonus
+               return race.base_strength
+       end
+
+       # The actual endurance of the character.
+       fun total_endurance: Int do
+               if career != null then return race.base_endurance + career.endurance_bonus
+               return race.base_endurance
+       end
+
+       # The acutal intelligence of the character.
+       fun total_intelligence: Int do
+               if career != null then return race.base_intelligence + career.intelligence_bonus
+               return race.base_intelligence
+       end
+
+       # Maximum health of the character.
+       #
+       # Based on `total endurance * 10`.
+       fun max_health: Int do return total_endurance * 10
+
+       # The current `health` of the character.
+       #
+       # Starts at `max_health`.
+       var health: Int = max_health
+end
+
diff --git a/tests/test_prog/rpg/combat.nit b/tests/test_prog/rpg/combat.nit
new file mode 100644 (file)
index 0000000..46462af
--- /dev/null
@@ -0,0 +1,67 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# COmbat interactions between characters.
+module combat
+
+import character
+
+# Something that can be used to attack someone and inflict damage.
+interface Weapon
+       # Damage per second inflicted by this weapon.
+       fun dps: Float is abstract
+end
+
+# Something that can be combatted, it can `attack` and `defend`.
+#
+# World items can also be `Combatable`.
+# `defend` method is then used to determines how the object react to an attack
+# Some magical items can even `attack`.
+interface Combatable
+       fun hit_points: Int is abstract
+
+       # A `Combatable` can attack a `target` that is also a `Combatable`.
+       #
+       # Attack the `target` using `wepaon` and returns the number of inflicted hit points.
+       fun attack(target: Combatable, weapon: Weapon): Int is abstract
+
+       # Like `attack` but cannot be defended.
+       fun direct_attack(target: Combatable, weapon: Weapon): Int is abstract
+
+       # `Combatable` can defend against attacks.
+       #
+       # Defends against a number of received hit points and return the number of pared hit points.
+       #
+       # @param hit: damage received.
+       fun defend(hit: Int): Int is abstract
+
+       # Is the character still have hit_points?
+       fun is_dead: Bool do return hit_points > 0
+end
+
+# Characters are now `Comabatable`
+redef class Character
+       super Combatable
+
+       # Use character `health` to determines hit_points.
+       redef fun hit_points do return health
+end
+
+# Dwarves can be used as weapons.
+redef class Dwarf
+       super Weapon
+
+       # Dwarf `dps` are based on the dwarf `base_endurance` (represents weight here)
+       redef fun dps do return base_endurance.to_f / 10.0
+end
diff --git a/tests/test_prog/rpg/races.nit b/tests/test_prog/rpg/races.nit
new file mode 100644 (file)
index 0000000..9af0a00
--- /dev/null
@@ -0,0 +1,79 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Races of the game.
+#
+# All characters belong to a `Race`.
+#
+# Available races:
+#
+#  * `Human`
+#  * `Dwarf`
+#  * `Elf`
+module races
+
+import platform
+
+# Race determines basic characteristics and what the character will be able to do in life.
+#
+# These are base characteristics, they cannot be changed
+# but you can add new ones if needed using refinement.
+# Objects and spells cannot change those characteristics.
+abstract class Race
+
+       # Used to represents how strong the race is.
+       var base_strength: Int
+
+       # Used to represents how the race can absorb damage.
+       var base_endurance: Int
+
+       # Is this race smart?
+       var base_intelligence: Int
+
+       init do end
+end
+
+# Humans are able to do everithing.
+class Human
+       super Race
+
+       init do
+               self.base_strength = 50
+               self.base_endurance = 50
+               self.base_intelligence = 50
+       end
+end
+
+# Dwarves make strong warriors.
+class Dwarf
+       super Race
+
+       init do
+               self.base_strength = 60
+               self.base_endurance = 50
+               self.base_intelligence = 40
+       end
+end
+
+# Elves make good magicians.
+class Elf
+       super Race
+
+       init do
+               self.base_strength = 40
+               self.base_endurance = 40
+               self.base_intelligence = 70
+       end
+end
+
diff --git a/tests/test_prog/rpg/rpg.nit b/tests/test_prog/rpg/rpg.nit
new file mode 100644 (file)
index 0000000..7356741
--- /dev/null
@@ -0,0 +1,22 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A worlg RPG abstraction.
+module rpg
+
+import races
+import careers
+import character
+import combat
+
diff --git a/tests/test_prog/test_prog.nit b/tests/test_prog/test_prog.nit
new file mode 100644 (file)
index 0000000..d24c62f
--- /dev/null
@@ -0,0 +1,27 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A test program with a fake model to check model tools.
+module test_prog
+
+import rpg
+import game
+
+class Starter
+       fun start do end
+end
+
+var starter = new Starter
+starter.start
+