Merge: Faster lookup
authorJean Privat <jean@pryen.org>
Tue, 9 Aug 2016 01:18:27 +0000 (21:18 -0400)
committerJean Privat <jean@pryen.org>
Tue, 9 Aug 2016 01:18:27 +0000 (21:18 -0400)
This series improves the lookup strategies:

* special fast track for the virtual type SELF
* choose to iterate on the mclassdefs instead of the mpropdefs if they are less numerous.

This mainly limit the degenerative cases that where discovered while investigating the slowdown of #2223

with nitc/nitc/nitc:
before: 0m7.168s
after: 0m6.232s

with nitpick ../contrib:
before: 0m20.928s
after: 0m19.432s

Pull-Request: #2245
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

30 files changed:
lib/gamnit/android_two_fingers_motion.nit [new file with mode: 0644]
lib/gamnit/flat.nit
lib/gen_nit.nit
lib/github/api.nit
lib/github/wallet.nit [new file with mode: 0644]
lib/json/serialization.nit
lib/mongodb/mongodb.nit
lib/mongodb/native_mongodb.nit
lib/popcorn/pop_auth.nit [new file with mode: 0644]
lib/popcorn/pop_config.nit [new file with mode: 0644]
lib/popcorn/pop_handlers.nit
lib/popcorn/pop_middlewares.nit
lib/popcorn/pop_repos.nit [new file with mode: 0644]
lib/popcorn/pop_validation.nit [new file with mode: 0644]
lib/serialization/serialization.nit
src/frontend/serialization_phase.nit
src/metrics/mendel_metrics.nit
src/modelize/modelize_class.nit
src/nitserial.nit
tests/sav/nitce/test_binary_deserialization_alt1.res
tests/sav/nitce/test_json_deserialization_alt1.res
tests/sav/nitce/test_json_deserialization_alt3.res
tests/sav/nitserial_args1.res
tests/sav/test_json_deserialization_alt3.res
tests/sav/test_object_class_kind_alt1.res [new file with mode: 0644]
tests/sav/test_object_class_kind_alt2.res [new file with mode: 0644]
tests/sav/test_object_class_kind_alt3.res [new file with mode: 0644]
tests/sav/test_object_class_kind_alt4.res [new file with mode: 0644]
tests/test_json_deserialization.nit
tests/test_object_class_kind.nit [new file with mode: 0644]

diff --git a/lib/gamnit/android_two_fingers_motion.nit b/lib/gamnit/android_two_fingers_motion.nit
new file mode 100644 (file)
index 0000000..b8329ef
--- /dev/null
@@ -0,0 +1,113 @@
+# 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.
+
+# Two fingers camera manipulation, scroll and pinch to zoom
+#
+# Provides the main service `EulerCamera::accept_two_fingers_motion`.
+module android_two_fingers_motion
+
+# TODO add an abstraction when another platform supports it
+
+import gamnit
+import cameras
+import android
+
+redef class EulerCamera
+       # Smoothened history of pointers in the current motion event
+       private var last_motion_pointers = new HashMap[Int, Point[Float]] is lazy
+
+       # Start time of the current motion event
+       private var last_motion_start: Int = -1
+
+       # Move and zoom (set `position`) from a two finger pinch and slide option
+       #
+       # Returns `true` if the event is intercepted.
+       #
+       # Should be called from `App::accept_event` before accepting pointer events:
+       #
+       # ~~~nitish
+       # redef class App
+       #     redef fun accept_event(event)
+       #     do
+       #         if world_camera.accept_two_fingers_motion(event) then return true
+       #
+       #         # Handle other events...
+       #     end
+       # end
+       # ~~~
+       fun accept_two_fingers_motion(event: InputEvent): Bool
+       do
+               if not event isa AndroidMotionEvent then return false
+
+               if event.pointers.length < 2 then
+                       # Intercept leftovers of the last motion
+                       return event.down_time == last_motion_start
+               end
+
+               # Collect active pointer and their world position
+               var new_motion_pointers = new HashMap[Int, Point[Float]]
+               var ids = new Array[Int]
+               for pointer in event.pointers do
+                       var id = pointer.pointer_id
+                       ids.add id
+                       new_motion_pointers[id] = camera_to_world(pointer.x, pointer.y)
+               end
+
+               var last_motion_pointers = last_motion_pointers
+               if last_motion_start == event.down_time and
+                  last_motion_pointers.keys.has(ids[0]) and last_motion_pointers.keys.has(ids[1]) then
+                       # Continued motion event
+
+                       # Get new and old position for 2 fingers
+                       var new_motion_a = new_motion_pointers[ids[0]]
+                       var new_motion_b = new_motion_pointers[ids[1]]
+                       var prev_pos_a = last_motion_pointers[ids[0]]
+                       var prev_pos_b = last_motion_pointers[ids[1]]
+
+                       # Move camera
+                       var prev_middle_pos = prev_pos_a.lerp(prev_pos_b, 0.5)
+                       var new_middle_pos = new_motion_a.lerp(new_motion_b, 0.5)
+                       position.x -= new_middle_pos.x - prev_middle_pos.x
+                       position.y -= new_middle_pos.y - prev_middle_pos.y
+
+                       # Zoom camera
+                       var prev_dist = prev_pos_a.dist(prev_pos_b)
+                       var new_dist = new_motion_a.dist(new_motion_b)
+
+                       position.z = prev_dist * position.z / new_dist
+               else
+                       # Prepare for a new motion event
+                       last_motion_pointers.clear
+                       last_motion_start = event.down_time
+               end
+
+               # Keep a smooth history
+               for i in [0..1] do
+                       if last_motion_pointers.keys.has(ids[i]) then
+                               last_motion_pointers[ids[i]] = last_motion_pointers[ids[i]]*0.5 +
+                                                               new_motion_pointers[ids[i]]*0.5
+                       else last_motion_pointers[ids[i]] = new_motion_pointers[ids[i]]
+               end
+
+               return true
+       end
+end
+
+redef class Point[N]
+       private fun *(scalar: Numeric): Point[N]
+       do return new Point[N](x.mul(scalar), y.mul(scalar))
+
+       private fun +(other: Point[N]): Point[N]
+       do return new Point[N](x.add(other.x), y.add(other.y))
+end
index 45d1ce9..8501ef4 100644 (file)
@@ -43,6 +43,8 @@ import gamnit
 import gamnit::cameras
 import gamnit::limit_fps
 
+import android_two_fingers_motion is conditional(android)
+
 # Image to draw on screen
 class Sprite
 
index a9dc0ed..7af2044 100644 (file)
@@ -52,6 +52,9 @@ class NitModule
        # The module's name
        var name: Writable is writable
 
+       # Annotations on the module declaration
+       var annotations = new Array[Writable]
+
        # Imports from this module
        var imports = new Array[Writable]
 
@@ -64,7 +67,13 @@ class NitModule
                if header != null then add header
 
                var name = name
-               add "module {name}\n\n"
+               if annotations.is_empty then
+                       add "module {name}\n\n"
+               else
+                       add "module {name} is\n"
+                       for annotation in annotations do add "\t{annotation}\n"
+                       add "end\n\n"
+               end
 
                for i in imports do add "import {i}\n"
                add "\n"
index 59917c5..850bb6f 100644 (file)
@@ -145,6 +145,21 @@ class GithubAPI
                return res.as(JsonObject)
        end
 
+       # Get the Github logged user from `auth` token.
+       #
+       # Loads the `User` from the API or returns `null` if the user cannot be found.
+       #
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var user = api.load_auth_user
+       # assert user.login == "Morriar"
+       # ~~~
+       fun load_auth_user: nullable User do
+               var json = load_from_github("user")
+               if was_error then return null
+               return new User.from_json(self, json)
+       end
+
        # Get the Github user with `login`
        #
        # Loads the `User` from the API or returns `null` if the user cannot be found.
diff --git a/lib/github/wallet.nit b/lib/github/wallet.nit
new file mode 100644 (file)
index 0000000..985f7aa
--- /dev/null
@@ -0,0 +1,180 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.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.
+
+# Github OAuth tokens management
+#
+# When using batch mode with the `github` API, we can rapidly reach the rate
+# limit allowed by Github.
+#
+# One solution consists in using a wallet of tokens so we can rely on more than
+# one token and switch them when one become exhausted.
+#
+# ## Using the Github wallet to check tokens
+#
+# One functionality of the wallet is to check the validity of a token against
+# the API. `check_token` will return false if a token is invalid or exhausted.
+#
+# ~~~
+# var wallet = new GithubWallet
+# assert not wallet.check_token("this is a bad token")
+# ~~~
+#
+# ## Storing tokens
+#
+# The wallet can also be used to store tokens and check all of them.
+#
+# ~~~
+# wallet = new GithubWallet
+# wallet.add "some token"
+# wallet.add "some other token"
+# ~~~
+#
+# or
+#
+# ~~~
+# wallet = new GithubWallet.from_tokens(["token 1", "token 2"])
+# ~~~
+#
+# The `show_status` method can be used to display a summary of the validity of
+# each token in the wallet.
+#
+# ~~~
+# wallet.show_status
+# ~~~
+#
+# Will display something like this:
+#
+# ~~~raw
+# Wallet (2 tokens):
+#  * [OK] token 1
+#  * [KO] token 2
+# ~~~
+#
+# ## Using the wallet to obtain a Github API client
+#
+# Using the wallet you can cycle through tokens and obtain a new Github API client
+# instance with a fresh rate limit.
+#
+# ~~~
+# wallet = new GithubWallet.from_tokens(["token 1", "token 2"])
+# var api = wallet.api
+# ~~~
+#
+# The wallet will automatically cycle through the registered tokens to find one
+# that works.
+#
+# If no valid token is found after all of them was tried, the wallet returns a
+# client based on the last tried one.
+module wallet
+
+import github
+import console
+
+# Github OAuth tokens wallet
+class GithubWallet
+
+       # Github API tokens
+       var tokens = new Array[String]
+
+       # Init `self` from a collection of tokens
+       init from_tokens(tokens: Collection[String]) do self.tokens.add_all tokens
+
+       # Do not use colors in console output
+       var no_colors = false is writable
+
+       # Display debug information about the token processing
+       var verbose = false is writable
+
+       # Add a new token in the wallet
+       fun add(token: String) do tokens.add token
+
+       # Get an instance of GithubAPI based on the next available token.
+       #
+       # If no token is found, return an api based on the last exhausted token.
+       fun api: GithubAPI do
+               var token
+               if tokens.is_empty then
+                       message "No tokens, using `get_github_oauth`"
+                       token = get_github_oauth
+               else
+                       token = get_next_token
+                       var tried = 0
+                       while not check_token(token) do
+                               if tried >= tokens.length - 1 then
+                                       message "Exhausted all tokens, using {token}"
+                                       break
+                               end
+                               tried += 1
+                               token = get_next_token
+                       end
+               end
+               var api = new GithubAPI(token)
+               api.enable_cache = true
+               return api
+       end
+
+       # The current index in the `tokens` array
+       private var current_index = 0
+
+       # The current token in the `tokens` array based on `current_index`
+       fun current_token: String do return tokens[current_index]
+
+       # Get the next token in token `array` based on `current_token`.
+       #
+       # If the end of the list is reached, start again from the begining.
+       fun get_next_token: String do
+               if tokens.is_empty then
+                       return get_github_oauth
+               end
+               var token = current_token
+
+               if current_index < tokens.length - 1 then
+                       current_index += 1
+               else
+                       current_index = 0
+               end
+               return token
+       end
+
+       # Check if a token is valid
+       fun check_token(token: String): Bool do
+               message "Try token {token}"
+               var api = new GithubAPI(token)
+               api.load_repo("nitlang/nit")
+               return not api.was_error
+       end
+
+       # Print a message depending on `verbose`
+       fun message(message: String) do if verbose then print "[Github Wallet] {message}"
+
+       # Show wallet status in console
+       fun show_status do
+               if tokens.is_empty then
+                       print "Wallet is empty"
+                       return
+               end
+               print "Wallet ({tokens.length} tokens):"
+               for token in tokens do
+                       var status
+                       if check_token(token) then
+                               status = if no_colors then "OK" else "OK".green
+                       else
+                               status = if no_colors then "KO" else "KO".red
+                       end
+                       print " * [{status}] {token}"
+               end
+       end
+end
index 45578f2..6a66407 100644 (file)
@@ -600,23 +600,23 @@ redef class Collection[E]
        # Utility to serialize a normal Json array
        private fun serialize_to_pure_json(v: JsonSerializer)
        do
-                       v.stream.write "["
-                       v.indent_level += 1
-                       var is_first = true
-                       for e in self do
-                               if is_first then
-                                       is_first = false
-                               else v.stream.write ","
-                               v.new_line_and_indent
+               v.stream.write "["
+               v.indent_level += 1
+               var is_first = true
+               for e in self do
+                       if is_first then
+                               is_first = false
+                       else v.stream.write ","
+                       v.new_line_and_indent
 
-                               if not v.try_to_serialize(e) then
-                                       assert e != null # null would have been serialized
-                                       v.warn("element of type {e.class_name} is not serializable.")
-                               end
+                       if not v.try_to_serialize(e) then
+                               assert e != null # null would have been serialized
+                               v.warn("element of type {e.class_name} is not serializable.")
                        end
-                       v.indent_level -= 1
-                       v.new_line_and_indent
-                       v.stream.write "]"
+               end
+               v.indent_level -= 1
+               v.new_line_and_indent
+               v.stream.write "]"
        end
 end
 
@@ -636,6 +636,8 @@ redef class SimpleCollection[E]
                        v.stream.write """","""
                        v.new_line_and_indent
                        v.stream.write """"__items": """
+
+                       core_serialize_to v
                end
 
                serialize_to_pure_json v
@@ -654,10 +656,18 @@ redef class SimpleCollection[E]
                        v.notify_of_creation self
                        init
 
-                       var arr = v.path.last["__items"].as(SequenceRead[nullable Object])
+                       var arr = v.path.last.get_or_null("__items")
+                       if not arr isa SequenceRead[nullable Object] then
+                               # If there is nothing, we consider that it is an empty collection.
+                               if arr != null then v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}")
+                               return
+                       end
+
                        for o in arr do
                                var obj = v.convert_object(o)
-                               self.add obj
+                               if obj isa E then
+                                       add obj
+                               else v.errors.add new AttributeTypeError(self, "items", obj, "E")
                        end
                end
        end
@@ -669,9 +679,10 @@ redef class Map[K, V]
                # Register as pseudo object
                var id = v.cache.new_id_for(self)
 
+               v.stream.write "\{"
+               v.indent_level += 1
+
                if v.plain_json then
-                       v.stream.write "\{"
-                       v.indent_level += 1
                        var first = true
                        for key, val in self do
                                if not first then
@@ -688,12 +699,7 @@ redef class Map[K, V]
                                        v.stream.write "null"
                                end
                        end
-                       v.indent_level -= 1
-                       v.new_line_and_indent
-                       v.stream.write "\}"
                else
-                       v.stream.write "\{"
-                       v.indent_level += 1
                        v.new_line_and_indent
                        v.stream.write """"__kind": "obj", "__id": """
                        v.stream.write id.to_s
@@ -712,10 +718,12 @@ redef class Map[K, V]
                        v.stream.write """"__values": """
                        values.serialize_to_pure_json v
 
-                       v.indent_level -= 1
-                       v.new_line_and_indent
-                       v.stream.write "\}"
+                       core_serialize_to v
                end
+
+               v.indent_level -= 1
+               v.new_line_and_indent
+               v.stream.write "\}"
        end
 
        redef init from_deserializer(v)
@@ -726,12 +734,44 @@ redef class Map[K, V]
                        v.notify_of_creation self
                        init
 
-                       var length = v.deserialize_attribute("__length").as(Int)
-                       var keys = v.path.last["__keys"].as(SequenceRead[nullable Object])
-                       var values = v.path.last["__values"].as(SequenceRead[nullable Object])
+                       var length = v.deserialize_attribute("__length")
+                       var keys = v.path.last.get_or_null("__keys")
+                       var values = v.path.last.get_or_null("__values")
+
+                       # Length is optional
+                       if length == null and keys isa SequenceRead[nullable Object] then length = keys.length
+
+                       # Consistency check
+                       if not length isa Int or length < 0 or
+                          not keys isa SequenceRead[nullable Object] or
+                          not values isa SequenceRead[nullable Object] or
+                          keys.length != values.length or length != keys.length then
+
+                               # If there is nothing or length == 0, we consider that it is an empty Map.
+                               if (length != null and length != 0) or keys != null or values != null then
+                                       v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}")
+                               end
+                               return
+                       end
+
                        for i in length.times do
                                var key = v.convert_object(keys[i])
                                var value = v.convert_object(values[i])
+
+                               if not key isa K then
+                                       v.errors.add new AttributeTypeError(self, "keys", key, "K")
+                                       continue
+                               end
+
+                               if not value isa V then
+                                       v.errors.add new AttributeTypeError(self, "values", value, "V")
+                                       continue
+                               end
+
+                               if has_key(key) then
+                                       v.errors.add new Error("Deserialization Error: duplicated key '{key or else "null"}' in {self.class_name}, previous value overwritten")
+                               end
+
                                self[key] = value
                        end
                end
index ab4d387..80c4d4c 100644 (file)
@@ -162,9 +162,13 @@ end
 # Since the MongoDB notation is not JSON complient, the mongoc wrapper uses
 # a JSON based notation like `{"$oid": "hash"}`.
 # This is the notation returned by the `to_json` service.
-private class MongoObjectId
+class MongoObjectId
 
-       var native: BSONObjectId
+       private var native: BSONObjectId = new BSONObjectId
+
+       private init with_native(native: BSONObjectId) do
+               self.native = native
+       end
 
        # The unique ID as an MongoDB Object ID string.
        fun id: String do return native.id
@@ -270,7 +274,7 @@ class MongoClient
        private fun last_id: nullable MongoObjectId do
                var last_id = sys.last_mongoc_id
                if last_id == null then return null
-               return new MongoObjectId(last_id)
+               return new MongoObjectId.with_native(last_id)
        end
 
        # Set the last generated id or `null` to unset once used.
@@ -580,6 +584,41 @@ class MongoCollection
                return res
        end
 
+       # Applies an aggregation `pipeline` over the collection.
+       #
+       # ~~~
+       # var client = new MongoClient("mongodb://localhost:27017/")
+       # var col = client.database("test").collection("test_aggregate")
+       #
+       # col.drop
+       #
+       # col.insert("""{ "cust_id": "A123", "amount": 500, "status": "A"}""".parse_json.as(JsonObject))
+       # col.insert("""{ "cust_id": "A123", "amount": 250, "status": "A"}""".parse_json.as(JsonObject))
+       # col.insert("""{ "cust_id": "B212", "amount": 200, "status": "A"}""".parse_json.as(JsonObject))
+       # col.insert("""{ "cust_id": "A123", "amount": 300, "status": "D"}""".parse_json.as(JsonObject))
+       #
+       # var res = col.aggregate("""[
+       #       { "$match": { "status": "A" } },
+       #       { "$group": { "_id": "$cust_id", "total": { "$sum": "$amount" } } }
+       # ]""".parse_json.as(JsonArray))
+       #
+       # assert res[0].to_json == """{"_id":"B212","total":200}"""
+       # assert res[1].to_json == """{"_id":"A123","total":750}"""
+       # ~~~
+       fun aggregate(pipeline: JsonArray): Array[JsonObject] do
+               var q = new JsonObject
+               q["pipeline"] = pipeline
+               var res = new Array[JsonObject]
+               var c = native.aggregate(q.to_bson.native)
+               if c == null then return res
+               var cursor = new MongoCursor(c)
+               while cursor.is_ok do
+                       res.add cursor.item
+                       cursor.next
+               end
+               return res
+       end
+
        # Retrieves statistics about the collection.
        #
        # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
index fb8b305..ba9b5e3 100644 (file)
@@ -114,12 +114,23 @@ end
 # * a 2-byte process id (Big Endian), and
 # * a 3-byte counter (Big Endian), starting with a random value.
 extern class BSONObjectId `{ bson_oid_t * `}
+
+       # Generates a new `bson_oid_t`.
+       new `{
+               bson_oid_t *self = malloc(sizeof(bson_oid_t));
+               bson_oid_init(self, NULL);
+               return self;
+       `}
+
        # Object id.
        fun id: String import NativeString.to_s_with_copy `{
                char str[25];
                bson_oid_to_string(self, str);
                return NativeString_to_s_with_copy(str);
        `}
+
+       # Destroy `self`.
+       fun destroy `{ free(self); `}
 end
 
 redef class Sys
@@ -433,6 +444,24 @@ extern class NativeMongoCollection `{ mongoc_collection_t * `}
                return NativeMongoCursor_as_nullable(cursor);
        `}
 
+       # Wrapper for `mongoc_collection_aggregate()`.
+       #
+       # This function shall execute an aggregation `pipeline` on the underlying collection.
+       #
+       # The `pipeline` parameter should contain a field named `pipeline` containing
+       # a BSON array of pipeline stages.
+       fun aggregate(pipeline: NativeBSON): nullable NativeMongoCursor import
+               NativeMongoCursor.as nullable, set_mongoc_error `{
+               bson_error_t error;
+               mongoc_cursor_t *cursor;
+               cursor = mongoc_collection_aggregate(self, MONGOC_QUERY_NONE, pipeline, NULL, NULL);
+               if (mongoc_cursor_error(cursor, &error)) {
+                       NativeMongoCollection_set_mongoc_error(self, &error);
+                       return null_NativeMongoCursor();
+               }
+               return NativeMongoCursor_as_nullable(cursor);
+       `}
+
        # Wrapper for `mongoc_collection_stats()`.
        #
        # This function is a helper to retrieve statistics about the collection.
diff --git a/lib/popcorn/pop_auth.nit b/lib/popcorn/pop_auth.nit
new file mode 100644 (file)
index 0000000..ca37876
--- /dev/null
@@ -0,0 +1,299 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.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.
+
+# Authentification handlers.
+#
+# For now, only Github OAuth is provided.
+#
+# See https://developer.github.com/v3/oauth/.
+#
+# This module provide 4 base classes that can be used together to implement a
+# Github OAuth handshake.
+#
+# Here an example of application using the Github Auth as login mechanism.
+#
+# There is 4 available routes:
+# * `/login`: redirects the user to the Github OAuth login page (see `GithubLogin`)
+# * `/profile`: shows the currently logged in user (see `Profile Handler`)
+# * `/logout`: logs out the user by destroying the entry from the session (see `GithubLogout`)
+# * `/oauth`: callback url for Github service after player login (see `GithubOAuthCallBack`)
+#
+# Routes redirection are handled at the OAuth service registration. Please see
+# https://developer.github.com/v3/oauth/#redirect-urls for more niformation on how
+# to configure your service to provide smouth redirections beween your routes.
+#
+# ~~~
+# import popcorn
+# import popcorn::pop_auth
+#
+# class ProfileHandler
+#      super Handler
+#
+#      redef fun get(req, res) do
+#              var session = req.session
+#              if session == null then
+#                      res.send "No session :("
+#                      return
+#              end
+#              var user = session.user
+#              if user == null then
+#                      res.send "Not logged in"
+#                      return
+#              end
+#              res.send "<h1>Hello {user.login}</h1>"
+#      end
+# end
+#
+# var client_id = "github client id"
+# var client_secret = "github client secret"
+#
+# var app = new App
+# app.use("/*", new SessionInit)
+# app.use("/login", new GithubLogin(client_id))
+# app.use("/oauth", new GithubOAuthCallBack(client_id, client_secret))
+# app.use("/logout", new GithubLogout)
+# app.use("/profile", new ProfileHandler)
+# app.listen("localhost", 3000)
+# ~~~
+#
+# Optionaly, you can use the `GithubUser` handler to provide access to the
+# Github user stored in session:
+#
+# ~~~
+# app.use("/api/user", new GithubUser)
+# ~~~
+module pop_auth
+
+import pop_handlers
+import github
+
+# Github OAuth login handler.
+#
+# See https://developer.github.com/v3/oauth/.
+class GithubLogin
+       super Handler
+
+       # Client ID delivered by GitHub for your application.
+       #
+       # See https://github.com/settings/applications/new.
+       var client_id: String is writable
+
+       # The URL in your application where users will be sent after authorization.
+       #
+       # If `null`, the URL used in application registration will be used.
+       #
+       # See https://developer.github.com/v3/oauth/#redirect-urls.
+       var redirect_uri: nullable String = null is writable
+
+       # A space delimited list of scopes.
+       #
+       # See https://developer.github.com/v3/oauth/#scopes.
+       var scope: nullable String = null is writable
+
+       # An optional and unguessable random string.
+       #
+       # It is used to protect against cross-site request forgery attacks.
+       var state: nullable String = null is writable
+
+       # Allow signup at login.
+       #
+       # Whether or not unauthenticated users will be offered an option to sign up
+       # for GitHub during the OAuth flow. The default is true.
+       #
+       # Use false in the case that a policy prohibits signups.
+       var allow_signup = true is writable
+
+       # Github OAuth login URL.
+       var auth_url = "https://github.com/login/oauth/authorize" is writable
+
+       # Build Github URL to OAuth service.
+       fun build_auth_redirect: String do
+               var url = "{auth_url}?client_id={client_id}&allow_signup={allow_signup}"
+               var redirect_uri = self.redirect_uri
+               if redirect_uri != null then url = "{url}&redirect_uri={redirect_uri}"
+               var scope = self.scope
+               if scope != null then url = "{url}&scope={scope}"
+               var state = self.state
+               if state != null then url = "{url}&state={state}"
+               return url
+       end
+
+       redef fun get(req, res) do res.redirect build_auth_redirect
+end
+
+# Get the authentification code and translate it to an access token.
+class GithubOAuthCallBack
+       super Handler
+
+       # The client ID delivered by GitHub for your application.
+       #
+       # See https://github.com/settings/applications/new.
+       var client_id: String is writable
+
+       # The client secret you received from Github when your registered your application.
+       var client_secret: String is writable
+
+       # The URL in your application where users will be sent after authorization.
+       #
+       # If `null`, the URL used in application registration will be used.
+       #
+       # See https://developer.github.com/v3/oauth/#redirect-urls.
+       var redirect_uri: nullable String is writable
+
+       # An optional and unguessable random string.
+       #
+       # It is used to protect against cross-site request forgery attacks.
+       var state: nullable String is writable
+
+       # Github OAuth token URL.
+       var token_url = "https://github.com/login/oauth/access_token" is writable
+
+       # Header map sent with the OAuth token request.
+       var headers: HeaderMap do
+               var map = new HeaderMap
+               map["Accept"] = "application/json"
+               return map
+       end
+
+       # Build the OAuth post data.
+       fun build_auth_body(code: String): HeaderMap do
+               var map = new HeaderMap
+               map["client_id"] = client_id
+               map["client_secret"] = client_secret
+               map["code"] = code
+               var redirect_uri = self.redirect_uri
+               if redirect_uri != null then map["redirect_uri"] = redirect_uri
+               var state = self.state
+               if state != null then map["state"] = state
+               return map
+       end
+
+       redef fun get(req, res) do
+               # Get OAuth code
+               var code = req.string_arg("code")
+               if code == null then
+                       res.error 401
+                       return
+               end
+
+               # Exchange it for an access token
+               var access_token = request_access_token(code)
+               if access_token == null then
+                       res.error 401
+                       return
+               end
+
+               # FIXME reinit curl before next request to avoid weird 404
+               curl = new Curl
+
+               # Load github user
+               var gh_api = new GithubAPI(access_token)
+               var user = gh_api.load_auth_user
+               if user == null then
+                       res.error 401
+                       return
+               end
+               # Set session and redirect to user page
+               var session = req.session
+               if session == null then
+                       res.error 500
+                       return
+               end
+               session.user = user
+               res.redirect redirect_uri or else "/"
+       end
+
+       # Request an access token from an access `code`.
+       private fun request_access_token(code: String): nullable String do
+               var request = new CurlHTTPRequest(token_url)
+               request.headers = headers
+               request.data = build_auth_body(code)
+               var response = request.execute
+               return parse_token_response(response)
+       end
+
+       # Parse the Github access_token response and extract the access_token.
+       private fun parse_token_response(response: CurlResponse): nullable String do
+               if response isa CurlResponseFailed then
+                       print "Request to Github OAuth failed"
+                       print "Requested URI: {token_url}"
+                       print "Error code: {response.error_code}"
+                       print "Error msg: {response.error_msg}"
+                       return null
+               else if response isa CurlResponseSuccess then
+                       var obj = response.body_str.parse_json
+                       if not obj isa JsonObject then
+                               print "Error: Cannot parse json response"
+                               print response.body_str
+                               return null
+                       end
+                       var access_token = obj.get_or_null("access_token")
+                       if not access_token isa String then
+                               print "Error: No `access_token` key in response"
+                               print obj.to_json
+                               return null
+                       end
+                       return access_token
+               end
+               return null
+       end
+end
+
+# Destroy user session and redirect to homepage.
+class GithubLogout
+       super Handler
+
+       # The URL in your application where users will be sent after logout.
+       #
+       # If `null`, the root uri `/` will be used.
+       var redirect_uri: nullable String is writable
+
+       redef fun get(req, res) do
+               var session = req.session
+               if session != null then
+                       session.user = null
+               end
+               res.redirect redirect_uri or else "/"
+       end
+end
+
+# Get the currently logged in user from session.
+class GithubUser
+       super Handler
+
+       # Get user from session or null.
+       fun get_session_user(req: HttpRequest): nullable User do
+               var session = req.session
+               if session == null then return null
+               var user = session.user
+               return user
+       end
+
+       redef fun get(req, res) do
+               var user = get_session_user(req)
+               if user == null then
+                       res.error 403
+                       return
+               end
+               res.json user.json
+       end
+end
+
+redef class Session
+
+       # Github user if logged in.
+       var user: nullable User = null is writable
+end
diff --git a/lib/popcorn/pop_config.nit b/lib/popcorn/pop_config.nit
new file mode 100644 (file)
index 0000000..1883b52
--- /dev/null
@@ -0,0 +1,191 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.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.
+
+# Configuration file and options for Popcorn apps
+#
+# `pop_config` provide a configuration framework for Popcorn apps based on ini
+# files.
+#
+# By default `AppConfig` provides `app.host` and `app.port` keys, it's all we
+# need to start an app:
+#
+# ~~~
+# import popcorn
+# import popcorn::pop_config
+#
+# # Parse app options
+# var opts = new AppOptions.from_args(args)
+#
+# # Build config from options
+# var config = new AppConfig.from_options(opts)
+#
+# # Use options
+# var app = new App
+# app.listen(config.app_host, config.app_port)
+# ~~~
+#
+# For more advanced uses, `AppConfig` and `AppOptions` can be specialized to
+# offer additional config options:
+#
+# ~~~
+# import popcorn
+# import popcorn::pop_config
+#
+# class MyConfig
+#      super AppConfig
+#
+#      # My secret code I don't want to share in my source repository
+#      var secret: String = value_or_default("secret", "my-secret")
+#
+#      redef init from_options(options) do
+#              super
+#              if options isa MyOptions then
+#                      var secret = options.opt_secret.value
+#                      if secret != null then self["secret"] = secret
+#              end
+#      end
+# end
+#
+# class MyOptions
+#      super AppOptions
+#
+#      var opt_secret = new OptionString("My secret string", "--secret")
+#
+#      redef init do
+#              super
+#              add_option opt_secret
+#      end
+# end
+#
+# class SecretHandler
+#      super Handler
+#
+#      # Config to use to access `secret`
+#      var config: MyConfig
+#
+#      redef fun get(req, res) do
+#              res.send config.secret
+#      end
+# end
+#
+# var opts = new MyOptions.from_args(args)
+# var config = new MyConfig.from_options(opts)
+#
+# var app = new App
+# app.use("/secret", new SecretHandler(config))
+# app.listen(config.app_host, config.app_port)
+# ~~~
+module pop_config
+
+import ini
+import opts
+
+# Configuration file for Popcorn apps
+#
+# ~~~
+# import popcorn
+# import popcorn::pop_config
+#
+# # Build config from default values
+# var config = new AppConfig("app.ini")
+#
+# # Change config values
+# config["app.port"] = 3001.to_s
+#
+# # Use options
+# var app = new App
+# app.listen(config.app_host, config.app_port)
+# ~~~
+class AppConfig
+       super ConfigTree
+
+       # Kind of options used by this config
+       type OPTIONS: AppOptions
+
+       # Default configuration file path
+       var default_config_file: String = "app.ini"
+
+       # Web app host name
+       #
+       # * key: `app.host`
+       # * default: `localhost`
+       var app_host: String is lazy do return value_or_default("app.host", "localhost")
+
+       # Web app port
+       #
+       # * key: `app.port`
+       # * default: `3000`
+       var app_port: Int is lazy do return value_or_default("app.port", "3000").to_i
+
+       # Init `self` from a `AppOptions` option values
+       init from_options(opts: OPTIONS) do
+               init(opts.opt_config.value or else default_config_file)
+               var opt_host = opts.opt_host.value
+               if opt_host != null then self["app.host"] = opt_host
+               var opt_port = opts.opt_port.value
+               if opt_port > 0 then self["app.port"] = opt_port.to_s
+       end
+
+       # Return the registered value for `key` or `default`
+       protected fun value_or_default(key: String, default: String): String do
+               return self[key] or else default
+       end
+end
+
+# Options configuration for Popcorn apps
+#
+# Use the `AppOptions` class in your app to parse command line args:
+# ~~~
+# import popcorn
+# import popcorn::pop_config
+#
+# # Parse app options
+# var opts = new AppOptions.from_args(args)
+#
+# # Build config from options
+# var config = new AppConfig.from_options(opts)
+#
+# # Use options
+# var app = new App
+# app.listen(config.app_host, config.app_port)
+# ~~~
+class AppOptions
+       super OptionContext
+
+       # Help option.
+       var opt_help = new OptionBool("Show this help message", "-h", "--help")
+
+       # Path to app config file.
+       var opt_config = new OptionString("Path to app config file", "--config")
+
+       # Host name to bind on (will overwrite the config one).
+       var opt_host = new OptionString("Host to bind the server on", "--host")
+
+       # Port number to bind on (will overwrite the config one).
+       var opt_port = new OptionInt("Port number to use", -1, "--port")
+
+       # You should redefined this method to add your options
+       init do
+               super
+               add_option(opt_help, opt_config, opt_host, opt_port)
+       end
+
+       # Initialize `self` and parse `args`
+       init from_args(args: Collection[String]) do
+               init
+               parse(args)
+       end
+end
index 5d84961..1a22db6 100644 (file)
@@ -458,6 +458,11 @@ redef class HttpResponse
                end
        end
 
+       # Write error as JSON and set the right content type header.
+       fun json_error(error: nullable Jsonable, status: nullable Int) do
+               json(error, status)
+       end
+
        # Redirect response to `location`
        fun redirect(location: String, status: nullable Int) do
                header["Location"] = location
index 518d9dc..bb25da0 100644 (file)
@@ -42,23 +42,68 @@ end
 class ConsoleLog
        super Handler
 
+       # Logger level
+       #
+       # * `0`: silent
+       # * `1`: errors
+       # * `2`: warnings
+       # * `3`: info
+       # * `4`: debug
+       #
+       # Request status are always logged, whatever the logger level is.
+       var level = 4 is writable
+
        # Do we want colors in the console output?
-       var colors = true
+       var no_colors = false
 
        redef fun all(req, res) do
                var clock = req.clock
                if clock != null then
-                       print "{req.method} {req.uri} {status(res)} ({clock.total}s)"
+                       log "{req.method} {req.uri} {status(res)} ({clock.total}s)"
                else
-                       print "{req.method} {req.uri} {status(res)}"
+                       log "{req.method} {req.uri} {status(res)}"
                end
        end
 
        # Colorize the request status.
        private fun status(res: HttpResponse): String do
-               if colors then return res.color_status
-               return res.status_code.to_s
+               if no_colors then return res.status_code.to_s
+               return res.color_status
        end
+
+       # Display a `message` with `level`.
+       #
+       # Message will only be displayed if `level <= self.level`.
+       # Colors will be used depending on `colors`.
+       #
+       # Use `0` for no coloration.
+       private fun display(level: Int, message: String) do
+               if level > self.level then return
+               if no_colors then
+                       print message
+                       return
+               end
+               if level == 0 then print message
+               if level == 1 then print message.red
+               if level == 2 then print message.yellow
+               if level == 3 then print message.blue
+               if level == 4 then print message.gray
+       end
+
+       # Display a message wathever the `level`
+       fun log(message: String) do display(0, message)
+
+       # Display a red error `message`.
+       fun error(message: String) do display(1, "[ERROR] {message}")
+
+       # Display a yellow warning `message`.
+       fun warning(message: String) do display(2, "[WARN] {message}")
+
+       # Display a blue info `message`.
+       fun info(message: String) do display(3, "[INFO] {message}")
+
+       # Display a gray debug `message`.
+       fun debug(message: String) do display(4, "[DEBUG] {message}")
 end
 
 redef class HttpRequest
@@ -69,9 +114,11 @@ end
 redef class HttpResponse
        # Return `self` status colored for console.
        fun color_status: String do
-               if status_code == 200 then return status_code.to_s.green
-               if status_code == 304 then return status_code.to_s.blue
-               if status_code == 404 then return status_code.to_s.yellow
-               return status_code.to_s.red
+               if status_code >= 100 and status_code < 200 then return status_code.to_s.gray
+               if status_code >= 200 and status_code < 300 then return status_code.to_s.green
+               if status_code >= 300 and status_code < 400 then return status_code.to_s.blue
+               if status_code >= 400 and status_code < 500 then return status_code.to_s.yellow
+               if status_code >= 500 and status_code < 600 then return status_code.to_s.red
+               return status_code.to_s
        end
 end
diff --git a/lib/popcorn/pop_repos.nit b/lib/popcorn/pop_repos.nit
new file mode 100644 (file)
index 0000000..9f9cfed
--- /dev/null
@@ -0,0 +1,317 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.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.
+
+# Repositories for data management.
+#
+# Repositories are used to apply persistence on instances (or **documents**).
+# Using repositories one can store and retrieve instance in a clean and maintenable
+# way.
+#
+# This module provides the base interface `Repository` that defines the persistence
+# services available in all kind of repos.
+# `JsonRepository` factorizes all repositories dedicated to Json data or objects
+# serializable to Json.
+#
+# `MongoRepository` is provided as a concrete example of repository.
+# It implements all the services from `Repository` using a Mongo database as backend.
+#
+# Repositories can be used in Popcorn app to manage your data persistence.
+# Here an example with a book management app:
+#
+# ~~~
+# # First we declare the `Book` class. It has to be serializable so it can be used
+# # within a `Repository`.
+#
+# import popcorn
+# import popcorn::pop_repos
+#
+# # Serializable book representation.
+# class Book
+#      serialize
+#      super Jsonable
+#
+#      # Book uniq ID
+#      var id: String = (new MongoObjectId).id is serialize_as "_id"
+#
+#      # Book title
+#      var title: String
+#
+#      # ... Other fields
+#
+#      redef fun to_s do return title
+#      redef fun ==(o) do return o isa SELF and id == o.id
+#      redef fun hash do return id.hash
+#      redef fun to_json do return serialize_to_json
+# end
+#
+# # We then need to subclass the `MongoRepository` to provide Book specific services.
+#
+# # Book repository for Mongo
+# class BookRepo
+#      super MongoRepository[Book]
+#
+#      # Find books by title
+#      fun find_by_title(title: String): Array[Book] do
+#              var q = new JsonObject
+#              q["title"] = title
+#              return find_all(q)
+#      end
+# end
+#
+# # The repository can be used in a Handler to manage book in a REST API.
+#
+# class BookHandler
+#      super Handler
+#
+#      var repo: BookRepo
+#
+#      # Return a json array of all books
+#      #
+#      # If the get parameters `title` is provided, returns a json array of books
+#      # matching the `title`.
+#      redef fun get(req, res) do
+#              var title = req.string_arg("title")
+#              if title == null then
+#                      res.json new JsonArray.from(repo.find_all)
+#              else
+#                      res.json new JsonArray.from(repo.find_by_title(title))
+#              end
+#      end
+#
+#      # Insert a new Book
+#      redef fun post(req, res) do
+#              var title = req.string_arg("title")
+#              if title == null then
+#                      res.error 400
+#                      return
+#              end
+#              var book = new Book(title)
+#              repo.save book
+#              res.json book
+#      end
+# end
+#
+# # Let's wrap it all together in a Popcorn app:
+#
+# # Init database
+# var mongo = new MongoClient("mongodb://localhost:27017/")
+# var db = mongo.database("tests_app_{100000.rand}")
+# var coll = db.collection("books")
+#
+# # Init app
+# var app = new App
+# var repo = new BookRepo(coll)
+# app.use("/books", new BookHandler(repo))
+# app.listen("localhost", 3000)
+# ~~~
+module pop_repos
+
+import serialization
+import json::serialization
+import mongodb
+
+# A Repository is an object that can store serialized instances.
+#
+# Repository is the base class of all kind of persistance processes. It offers
+# the base CRUD services to save (add/update), find and delete instances.
+#
+# Instances are stored in their serialized form. See the `serialization` package
+# for more documentation.
+interface Repository[E: Serializable]
+
+       # Kind of queries accepted
+       #
+       # Can be redefined to accept more precise queries depending on the backend used.
+       type QUERY: RepositoryQuery
+
+       # Find an instance by it's `id`
+       #
+       # `id` is an abstract thing at this stage
+       # TODO maybe introduce the `PrimaryKey` concept?
+       fun find_by_id(id: String): nullable E is abstract
+
+       # Find an instance based on `query`
+       fun find(query: QUERY): nullable E is abstract
+
+       # Find all instances based on `query`
+       #
+       # Using `query` == null will retrieve all the document in the repository.
+       fun find_all(query: nullable QUERY): Array[E] is abstract
+
+       # Save an `instance`
+       fun save(instance: E): Bool is abstract
+
+       # Remove the instance with `id`
+       fun remove_by_id(id: String): Bool is abstract
+
+       # Remove the instance based on `query`
+       fun remove(query: nullable QUERY): Bool is abstract
+
+       # Remove all instances
+       fun clear: Bool is abstract
+
+       # Serialize an `instance` to a String.
+       fun serialize(instance: nullable E): nullable String is abstract
+
+       # Deserialize a `string` to an instance.
+       fun deserialize(string: nullable String): nullable E is abstract
+end
+
+# An abstract Query representation.
+#
+# Since the kind of query available depends on the database backend choice or
+# implementation, this interface is used to provide a common type to all the
+# queries.
+#
+# Redefine `Repository::QUERY` to use your own kind of query.
+interface RepositoryQuery end
+
+# A Repository for JsonObjects.
+#
+# As for document oriented databases, Repository can be used to store and retrieve
+# Json object.
+# Serialization from/to Json is used to translate from/to nit instances.
+#
+# See `MongoRepository` for a concrete implementation example.
+interface JsonRepository[E: Serializable]
+       super Repository[E]
+
+       redef fun serialize(item) do
+               if item == null then return null
+               return item.serialize_to_json
+       end
+
+       redef fun deserialize(string) do
+               if string == null then return null
+               var deserializer = new JsonDeserializer(string)
+               return deserializer.deserialize.as(E)
+       end
+end
+
+# A Repository that uses MongoDB as backend.
+#
+# ~~~
+# import popcorn
+# import popcorn::pop_routes
+#
+# # First, let's create a User abstraction:
+#
+# # Serializable user representation.
+# class User
+#      serialize
+#      super Jsonable
+#
+#      # User uniq ID
+#      var id: String = (new MongoObjectId).id is serialize_as "_id"
+#
+#      # User login
+#      var login: String
+#
+#      # User password
+#      var password: String is writable
+#
+#      redef fun to_s do return login
+#      redef fun ==(o) do return o isa SELF and id == o.id
+#      redef fun hash do return id.hash
+#      redef fun to_json do return serialize_to_json
+# end
+#
+# # We then need to subclass the `MongoRepository` to provide User specific services:
+#
+# # User repository for Mongo
+# class UserRepo
+#      super MongoRepository[User]
+#
+#      # Find a user by its login
+#      fun find_by_login(login: String): nullable User do
+#              var q = new JsonObject
+#              q["login"] = login
+#              return find(q)
+#      end
+# end
+#
+# # The repository can then be used with User instances:
+#
+# # Init database
+# var mongo = new MongoClient("mongodb://localhost:27017/")
+# var db = mongo.database("tests")
+# var coll = db.collection("test_pop_repo_{100000.rand}")
+#
+# # Create a user repo to store User instances
+# var repo = new UserRepo(coll)
+#
+# # Create some users
+# repo.save(new User("Morriar", "1234"))
+# repo.save(new User("Alex", "password"))
+#
+# assert repo.find_all.length == 2
+# assert repo.find_by_login("Morriar").password == "1234"
+# repo.clear
+# assert repo.find_all.length == 0
+# ~~~
+class MongoRepository[E: Serializable]
+       super JsonRepository[E]
+
+       redef type QUERY: JsonObject
+
+       # MongoDB collection used to store objects
+       var collection: MongoCollection
+
+       redef fun find_by_id(id) do
+               var query = new JsonObject
+               query["_id"] = id
+               return find(query)
+       end
+
+       redef fun find(query) do
+               var res = collection.find(query)
+               if res == null then return null
+               return deserialize(res.to_json)
+       end
+
+       redef fun find_all(query) do
+               var res = new Array[E]
+               for e in collection.find_all(query or else new JsonObject) do
+                       res.add deserialize(e.to_json).as(E)
+               end
+               return res
+       end
+
+       redef fun save(item) do
+               var json = serialize(item).as(String)
+               var obj = json.parse_json.as(JsonObject)
+               return collection.save(obj)
+       end
+
+       redef fun remove_by_id(id) do
+               var query = new JsonObject
+               query["_id"] = id
+               return remove(query)
+       end
+
+       redef fun remove(query) do
+               return collection.remove(query or else new JsonObject)
+       end
+
+       redef fun clear do return collection.drop
+end
+
+# JsonObject can be used as a `RepositoryQuery`.
+#
+# See `mongodb` lib.
+redef class JsonObject
+       super RepositoryQuery
+end
diff --git a/lib/popcorn/pop_validation.nit b/lib/popcorn/pop_validation.nit
new file mode 100644 (file)
index 0000000..386bb24
--- /dev/null
@@ -0,0 +1,703 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.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.
+
+# Quick and easy validation framework for Json inputs
+#
+# Validators can be used in Popcorn apps to valid your json inputs before
+# data processing and persistence.
+#
+# Here an example with a Book management app. We use an ObjectValidator to validate
+# the books passed to the API in the `POST /books` handler.
+#
+# ~~~
+# import popcorn
+# import serialization
+#
+# # Serializable book representation.
+# class Book
+#      super Jsonable
+#
+#      # Book ISBN
+#      var isbn: String
+#
+#      # Book title
+#      var title: String
+#
+#      # Book image (optional)
+#      var img: nullable String
+#
+#      # Book price
+#      var price: Float
+# end
+#
+# class BookValidator
+#      super ObjectValidator
+#
+#      redef init do
+#              add new ISBNField("isbn")
+#              add new StringField("title", min_size=1, max_size=255)
+#              add new StringField("img", required=false)
+#              add new FloatField("price", min=0.0, max=999.0)
+#      end
+# end
+#
+# class BookHandler
+#      super Handler
+#
+#      # Insert a new Book
+#      redef fun post(req, res) do
+#              var validator = new BookValidator
+#              if not validator.validate(req.body) then
+#                      res.json_error(validator.validation, 400)
+#                      return
+#              end
+#              # TODO data persistence
+#      end
+# end
+# ~~~
+module pop_validation
+
+import json
+
+# The base class of all validators
+abstract class DocumentValidator
+
+       # Validation result
+       #
+       # Accessible to the client after the `validate` method has been called.
+       var validation: ValidationResult is noinit
+
+       # Validate the `document` input
+       #
+       # Result of the validation can be found in the `validation` attribute.
+       fun validate(document: String): Bool do
+               validation = new ValidationResult
+               return true
+       end
+end
+
+# Validation Result representation
+#
+# Can be convertted to a JsonObject so it can be reterned in a Json HttpResponse.
+#
+# Errors messages are grouped into *scopes*. A scope is a string that specify wich
+# field or document the error message is related to.
+#
+# See `HttpResponse::json_error`.
+class ValidationResult
+       super Jsonable
+
+       # Object parsed during validation
+       #
+       # Can be used as a quick way to access the parsed JsonObject instead of
+       # reparsing it during the answer.
+       #
+       # See `ObjectValidator`.
+       var object: nullable JsonObject = null is writable
+
+       # Array parsed during validation
+       #
+       # Can be used as a quick way to access the parsed JsonArray instead of
+       # reparsing it during the answer.
+       #
+       # See `ArrayValidator`.
+       var array: nullable JsonArray = null is writable
+
+       # Errors found during validation
+       #
+       # Errors are grouped by scope.
+       var errors = new HashMap[String, Array[String]]
+
+       # Generate a new error `message` into `scope`
+       fun add_error(scope, message: String) do
+               if not errors.has_key(scope) then
+                       errors[scope] = new Array[String]
+               end
+               errors[scope].add message
+       end
+
+       # Get the errors for `scope`
+       fun error(scope: String): Array[String] do
+               if not errors.has_key(scope) then
+                       return new Array[String]
+               end
+               return errors[scope]
+       end
+
+       # Does `self` contains `errors`?
+       fun has_error: Bool do return errors.not_empty
+
+       # Render self as a JsonObject
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["has_error"] = has_error
+               var e = new JsonObject
+               for k, v in errors do
+                       e[k] = new JsonArray.from(v)
+               end
+               obj["errors"] = e
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+
+       # Returns the validation result as a pretty formated string
+       fun to_pretty_string: String do
+               var b = new Buffer
+               if not has_error then
+                       b.append "Everything is correct\n"
+               else
+                       b.append "There is errors\n\n"
+                       for k, v in errors do
+                               b.append "{k}:\n"
+                               for vv in v do
+                                       b.append "\t{vv}\n"
+                               end
+                               b.append "\n"
+                       end
+               end
+               return b.write_to_string
+       end
+end
+
+# Check a JsonObject
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new RequiredField("id", required = true)
+# validator.add new StringField("login", min_size=4)
+# validator.add new IntField("age", min=0, max=100)
+# assert not validator.validate("""{}""")
+# assert not validator.validate("""[]""")
+# assert validator.validate("""{ "id": "", "login": "Alex", "age": 10 }""")
+# ~~~
+class ObjectValidator
+       super DocumentValidator
+
+       # Validators to apply on the object
+       var validators = new Array[FieldValidator]
+
+       redef fun validate(document) do
+               super
+               var json = document.parse_json
+               if json == null then
+                       validation.add_error("document", "Expected JsonObject got `null`")
+                       return false
+               end
+               return validate_json(json)
+       end
+
+       # Validate a Jsonable input
+       fun validate_json(json: Jsonable): Bool do
+               if not json isa JsonObject then
+                       validation.add_error("document", "Expected JsonObject got `{json.class_name}`")
+                       return false
+               end
+               validation.object = json
+               for validator in validators do
+                       var res = validator.validate_field(self, json)
+                       if not res then return false
+               end
+               return true
+       end
+
+       # Add a validator
+       fun add(validator: FieldValidator) do validators.add validator
+end
+
+# Check a JsonArray
+# ~~~
+# var validator = new ArrayValidator
+# assert not validator.validate("""{}""")
+# assert validator.validate("""[]""")
+# assert validator.validate("""[ "id", 10, {} ]""")
+#
+# validator = new ArrayValidator(allow_empty=false)
+# assert not validator.validate("""[]""")
+# assert validator.validate("""[ "id", 10, {} ]""")
+#
+# validator = new ArrayValidator(length=3)
+# assert not validator.validate("""[]""")
+# assert validator.validate("""[ "id", 10, {} ]""")
+# ~~~
+class ArrayValidator
+       super DocumentValidator
+
+       # Allow empty arrays (default: true)
+       var allow_empty: nullable Bool
+
+       # Check array length (default: no check)
+       var length: nullable Int
+
+       redef fun validate(document) do
+               super
+               var json = document.parse_json
+               if json == null then
+                       validation.add_error("document", "Expected JsonArray got `null`")
+                       return false
+               end
+               return validate_json(json)
+       end
+
+       # Validate a Jsonable input
+       fun validate_json(json: Jsonable): Bool do
+               if not json isa JsonArray then
+                       validation.add_error("document", "Expected JsonArray got `{json.class_name}`")
+                       return false
+               end
+               validation.array = json
+               var allow_empty = self.allow_empty
+               if json.is_empty and (allow_empty != null and not allow_empty) then
+                       validation.add_error("document", "Cannot be empty")
+                       return false
+               end
+               var length = self.length
+               if length != null and json.length != length then
+                       validation.add_error("document", "Array length must be exactly `{length}`")
+                       return false
+               end
+
+               return true
+       end
+end
+
+# Something that can validate a JsonObject field
+abstract class FieldValidator
+
+       # Field to validate
+       var field: String
+
+       # Validate `field` in `obj`
+       fun validate_field(v: ObjectValidator, obj: JsonObject): Bool is abstract
+end
+
+# Check if a field exists
+#
+# ~~~
+# var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
+# var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
+# var json3 = """{ "field1": "", "field2": "foo" }"""
+#
+# var validator = new ObjectValidator
+# validator.add new RequiredField("field1")
+# validator.add new RequiredField("field2")
+# validator.add new RequiredField("field3")
+# validator.add new RequiredField("field4", required=false)
+#
+# assert validator.validate(json1)
+# assert validator.validate(json2)
+# assert not validator.validate(json3)
+# assert validator.validation.error("field3") == ["Required field"]
+# ~~~
+class RequiredField
+       super FieldValidator
+
+       # Is this field required?
+       var required: nullable Bool
+
+       redef fun validate_field(v, obj) do
+               var required = self.required
+               if (required != null and required or required == null) and not obj.has_key(field) then
+                       v.validation.add_error(field, "Required field")
+                       return false
+               end
+               return true
+       end
+end
+
+# Check if a field is a String
+#
+# `min_size` and `max_size` are optional
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new StringField("field", required=false)
+# assert validator.validate("""{}""")
+#
+# validator = new ObjectValidator
+# validator.add new StringField("field")
+# assert not validator.validate("""{}""")
+# assert not validator.validate("""{ "field": 10 }""")
+#
+# validator = new ObjectValidator
+# validator.add new StringField("field", min_size=3)
+# assert validator.validate("""{ "field": "foo" }""")
+# assert not validator.validate("""{ "field": "fo" }""")
+# assert not validator.validate("""{ "field": "" }""")
+#
+# validator = new ObjectValidator
+# validator.add new StringField("field", max_size=3)
+# assert validator.validate("""{ "field": "foo" }""")
+# assert not validator.validate("""{ "field": "fooo" }""")
+#
+# validator = new ObjectValidator
+# validator.add new StringField("field", min_size=3, max_size=5)
+# assert not validator.validate("""{ "field": "fo" }""")
+# assert validator.validate("""{ "field": "foo" }""")
+# assert validator.validate("""{ "field": "foooo" }""")
+# assert not validator.validate("""{ "field": "fooooo" }""")
+# ~~~
+class StringField
+       super RequiredField
+
+       # String min size (default: not checked)
+       var min_size: nullable Int
+
+       # String max size (default: not checked)
+       var max_size: nullable Int
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if val == null then
+                       if required == null or required == true then
+                               v.validation.add_error(field, "Expected String got `null`")
+                               return false
+                       else
+                               return true
+                       end
+               end
+               if not val isa String then
+                       v.validation.add_error(field, "Expected String got `{val.class_name}`")
+                       return false
+               end
+               var min_size = self.min_size
+               if min_size != null and val.length < min_size then
+                       v.validation.add_error(field, "Must be at least `{min_size} characters long`")
+                       return false
+               end
+               var max_size = self.max_size
+               if max_size != null and val.length > max_size then
+                       v.validation.add_error(field, "Must be at max `{max_size} characters long`")
+                       return false
+               end
+               return true
+       end
+end
+
+# Check if a field is an Int
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new IntField("field", required=false)
+# assert validator.validate("""{}""")
+#
+# validator = new ObjectValidator
+# validator.add new IntField("field")
+# assert not validator.validate("""{}""")
+# assert not validator.validate("""{ "field": "foo" }""")
+# assert validator.validate("""{ "field": 10 }""")
+#
+# validator = new ObjectValidator
+# validator.add new IntField("field", min=3)
+# assert validator.validate("""{ "field": 3 }""")
+# assert not validator.validate("""{ "field": 2 }""")
+#
+# validator = new ObjectValidator
+# validator.add new IntField("field", max=3)
+# assert validator.validate("""{ "field": 3 }""")
+# assert not validator.validate("""{ "field": 4 }""")
+#
+# validator = new ObjectValidator
+# validator.add new IntField("field", min=3, max=5)
+# assert not validator.validate("""{ "field": 2 }""")
+# assert validator.validate("""{ "field": 3 }""")
+# assert validator.validate("""{ "field": 5 }""")
+# assert not validator.validate("""{ "field": 6 }""")
+# ~~~
+class IntField
+       super RequiredField
+
+       # Min value (default: not checked)
+       var min: nullable Int
+
+       # Max value (default: not checked)
+       var max: nullable Int
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if val == null then
+                       if required == null or required == true then
+                               v.validation.add_error(field, "Expected Int got `null`")
+                               return false
+                       else
+                               return true
+                       end
+               end
+               if not val isa Int then
+                       v.validation.add_error(field, "Expected Int got `{val.class_name}`")
+                       return false
+               end
+               var min = self.min
+               if min != null and val < min then
+                       v.validation.add_error(field, "Must be greater or equal to `{min}`")
+                       return false
+               end
+               var max = self.max
+               if max != null and val > max then
+                       v.validation.add_error(field, "Must be smaller or equal to `{max}`")
+                       return false
+               end
+               return true
+       end
+end
+
+# Check if a field is a Float
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new FloatField("field", required=false)
+# assert validator.validate("""{}""")
+#
+# validator = new ObjectValidator
+# validator.add new FloatField("field")
+# assert not validator.validate("""{}""")
+# assert not validator.validate("""{ "field": "foo" }""")
+# assert validator.validate("""{ "field": 10.5 }""")
+#
+# validator = new ObjectValidator
+# validator.add new FloatField("field", min=3.0)
+# assert validator.validate("""{ "field": 3.0 }""")
+# assert not validator.validate("""{ "field": 2.0 }""")
+#
+# validator = new ObjectValidator
+# validator.add new FloatField("field", max=3.0)
+# assert validator.validate("""{ "field": 3.0 }""")
+# assert not validator.validate("""{ "field": 4.0 }""")
+#
+# validator = new ObjectValidator
+# validator.add new FloatField("field", min=3.0, max=5.0)
+# assert not validator.validate("""{ "field": 2.0 }""")
+# assert validator.validate("""{ "field": 3.0 }""")
+# assert validator.validate("""{ "field": 5.0 }""")
+# assert not validator.validate("""{ "field": 6.0 }""")
+# ~~~
+class FloatField
+       super RequiredField
+
+       # Min value (default: not checked)
+       var min: nullable Float
+
+       # Max value (default: not checked)
+       var max: nullable Float
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if val == null then
+                       if required == null or required == true then
+                               v.validation.add_error(field, "Expected Float got `null`")
+                               return false
+                       else
+                               return true
+                       end
+               end
+               if not val isa Float then
+                       v.validation.add_error(field, "Expected Float got `{val.class_name}`")
+                       return false
+               end
+               var min = self.min
+               if min != null and val < min then
+                       v.validation.add_error(field, "Must be smaller or equal to `{min}`")
+                       return false
+               end
+               var max = self.max
+               if max != null and val > max then
+                       v.validation.add_error(field, "Must be greater or equal to `{max}`")
+                       return false
+               end
+               return true
+       end
+end
+
+# Check that a field is a JsonObject
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new RequiredField("id", required = true)
+# var user_val = new ObjectField("user")
+# user_val.add new RequiredField("id", required = true)
+# user_val.add new StringField("login", min_size=4)
+# validator.add user_val
+# assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
+# assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
+# ~~~
+class ObjectField
+       super RequiredField
+       super ObjectValidator
+
+       redef var validation = new ValidationResult
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if val == null then
+                       if required == null or required == true then
+                               v.validation.add_error(field, "Expected Object got `null`")
+                               return false
+                       else
+                               return true
+                       end
+               end
+               var res = validate_json(val)
+               for field, messages in validation.errors do
+                       for message in messages do v.validation.add_error("{self.field}.{field}", message)
+               end
+               return res
+       end
+end
+
+# Check that a field is a JsonArray
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new RequiredField("id", required = true)
+# validator.add new ArrayField("orders", allow_empty=false)
+# assert not validator.validate("""{ "id": "", "orders": [] }""")
+# assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
+# ~~~
+class ArrayField
+       super RequiredField
+       super ArrayValidator
+
+       autoinit field=, required=, allow_empty=, length=
+
+       redef var validation = new ValidationResult
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if val == null then
+                       if required == null or required == true then
+                               v.validation.add_error(field, "Expected Array got `null`")
+                               return false
+                       else
+                               return true
+                       end
+               end
+               var res = validate_json(val)
+               for field, messages in validation.errors do
+                       for message in messages do v.validation.add_error("{self.field}.{field}", message)
+               end
+               return res
+       end
+end
+
+# Check if two fields values match
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new FieldsMatch("field1", "field2")
+#
+# assert validator.validate("""{ "field1": {}, "field2": {} }""")
+# assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
+# assert validator.validate("""{ "field1": null, "field2": null }""")
+# assert validator.validate("""{}""")
+#
+# assert not validator.validate("""{ "field1": {}, "field2": [] }""")
+# assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
+# assert not validator.validate("""{ "field1": null, "field2": "" }""")
+# assert not validator.validate("""{ "field1": "foo" }""")
+# ~~~
+class FieldsMatch
+       super FieldValidator
+
+       # Other field to compare with
+       var other: String
+
+       redef fun validate_field(v, obj) do
+               var val1 = obj.get_or_null(field)
+               var val2 = obj.get_or_null(other)
+               if val1 != val2 then
+                       v.validation.add_error(field, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
+                       return false
+               end
+               return true
+       end
+end
+
+# Check if a field match a regular expression
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
+# assert not validator.validate("""{ "title": "foo" }""")
+# assert validator.validate("""{ "title": "Foo" }""")
+# ~~~
+class RegexField
+       super RequiredField
+
+       autoinit field, re, required
+
+       # Regular expression to match
+       var re: Regex
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if val == null then
+                       if required == null or required == true then
+                               v.validation.add_error(field, "Expected String got `null`")
+                               return false
+                       else
+                               return true
+                       end
+               end
+               if not val isa String then
+                       v.validation.add_error(field, "Expected String got `{val.class_name}`")
+                       return false
+               end
+               if not val.has(re) then
+                       v.validation.add_error(field, "Does not match `{re.string}`")
+                       return false
+               end
+               return true
+       end
+end
+
+# Check if a field is a valid email
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new EmailField("email")
+# assert not validator.validate("""{ "email": "" }""")
+# assert not validator.validate("""{ "email": "foo" }""")
+# assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
+# assert validator.validate("""{ "email": "a+b@c.d" }""")
+# ~~~
+class EmailField
+       super RegexField
+
+       autoinit field, required
+
+       redef var re = "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
+end
+
+# Check if a field is a valid ISBN
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new ISBNField("isbn")
+# assert not validator.validate("""{ "isbn": "foo" }""")
+# assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
+# ~~~
+class ISBNField
+       super RegexField
+
+       autoinit field, required
+
+       redef var re = "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
+end
index 8ae8d2f..0ad3019 100644 (file)
@@ -143,6 +143,8 @@ end
 class AttributeTypeError
        super Error
 
+       autoinit receiver, attribute_name, attribute, expected_type
+
        # Parent object of the problematic attribute
        var receiver: Object
 
@@ -155,8 +157,7 @@ class AttributeTypeError
        # Name of the type expected for `attribute`
        var expected_type: String
 
-       redef fun to_s
-       do
+       redef var message is lazy do
                var attribute = attribute
                var found_type = if attribute != null then attribute.class_name else "null"
 
index 8b3de28..bb5cb8d 100644 (file)
@@ -237,20 +237,27 @@ do
 
                        var n_type = attribute.n_type
                        var type_name
+                       var type_name_pretty
                        if n_type == null then
                                # Use a place holder, we will replace it with the inferred type after the model phases
                                type_name = toolcontext.place_holder_type_name
+                               type_name_pretty = "Unknown type"
                        else
                                type_name = n_type.type_name
+                               type_name_pretty = type_name
                        end
                        var name = attribute.name
 
-                       code.add """
+                       if type_name == "nullable Object" then
+                               # Don't type check
+                               code.add """
+       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}")
+"""
+                       else code.add """
        var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}")
        if not {{{name}}} isa {{{type_name}}} then
                # Check if it was a subjectent error
-               v.errors.add new AttributeTypeError("TODO remove this arg on c_src regen",
-                       self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
+               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name_pretty}}}")
 
                # Clear subjacent error
                if v.keep_going == false then return
index 1eb4e5b..5bd6f97 100644 (file)
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# The mndel model helps to understand class hierarchies
+# The Mendel model helps to understand class hierarchies.
 #
 # It provides metrics to extract interesting classes:
 #
@@ -38,9 +38,9 @@
 # * replacers that have less redefinitions that call super than not calling it
 #
 # For more details see
-#  Mendel: A Model, Metrics and Rules to Understan Class Hierarchies
-#  S. Denier and Y. Gueheneuc
-#  in Proceedings of the 16th IEEE International Conference on Program Comprehension (OCPC'08)
+# “Mendel: A Model, Metrics and Rules to Understand Class Hierarchies,”
+# by S. Denier and Y. Gueheneuc,
+# in *Proceedings of the 16th IEEE International Conference on Program Comprehension* (OCPC'08).
 module mendel_metrics
 
 import metrics_base
index 045e829..8164668 100644 (file)
@@ -104,11 +104,6 @@ redef class ModelBuilder
                end
 
                if mclass == null then
-                       if nclassdef isa AStdClassdef and nclassdef.n_kwredef != null then
-                               error(nclassdef, "Redef Error: no imported class `{name}` to refine.")
-                               return
-                       end
-
                        # Check for conflicting class full-names in the package
                        if mmodule.mgroup != null and mvisibility >= protected_visibility then
                                var mclasses = model.get_mclasses_by_name(name)
@@ -279,10 +274,19 @@ redef class ModelBuilder
                if mclassdef.is_intro and objectclass != null then
                        if mclass.kind == extern_kind and mclass.name != "Pointer" then
                                # it is an extern class, but not a Pointer
+                               if pointerclass == null then
+                                       error(nclassdef, "Error: `Pointer` must be defined first.")
+                                       return
+                               end
                                if specpointer then supertypes.add pointerclass.mclass_type
-                       else if specobject and mclass.name != "Object" then
-                               # it is a standard class without super class (but is not Object)
-                               supertypes.add objectclass.mclass_type
+                       else if specobject then
+                               if mclass.name != "Object" then
+                                       # it is a standard class without super class (but is not Object)
+                                       supertypes.add objectclass.mclass_type
+                               else if mclass.kind != interface_kind then
+                                       error(nclassdef, "Error: `Object` must be an {interface_kind}.")
+                                       return
+                               end
                        end
                end
 
@@ -308,7 +312,6 @@ redef class ModelBuilder
        end
 
        # Build the classes of the module `nmodule`.
-       # REQUIRE: classes of imported modules are already build. (let `phase` do the job)
        private fun build_classes(nmodule: AModule)
        do
                # Force building recursively
index 9c8a07b..f7481d4 100644 (file)
@@ -158,6 +158,7 @@ for mmodule in mmodules do
        if importations == null then importations = target_modules
 
        var nit_module = new NitModule(module_name)
+       nit_module.annotations.add """no_warning("property-conflict")"""
        nit_module.header = """
 # This file is generated by nitserial
 # Do not modify, but you can redef
index d99f2e7..730ae5c 100644 (file)
@@ -20,7 +20,7 @@
 <D: <B: <A: false b 123.123 2345 new line ->
 <- false p4ssw0rd> 1111        f"\r\/> true>
 
-Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::a` expected `PlaceHolderTypeWhichShouldNotExist`, got `null`, Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::b` expected `PlaceHolderTypeWhichShouldNotExist`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::a` expected `Unknown type`, got `null`, Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::b` expected `Unknown type`, got `null`
 # Src:
 <E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
 # Dst:
@@ -28,7 +28,7 @@ Deserialization Error: Doesn't know how to deserialize class "Array", Deserializ
 
 Deserialization Error: Doesn't know how to deserialize class "F"
 Deserialization Error: Doesn't know how to deserialize class "F"
-Deserialization Error: Doesn't know how to deserialize class "HashSet", Deserialization Error: Wrong type on `G::hs` expected `PlaceHolderTypeWhichShouldNotExist`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArraySet", Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "HashMap", Deserialization Error: Wrong type on `G::hm` expected `PlaceHolderTypeWhichShouldNotExist`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArrayMap", Deserialization Error: Wrong type on `G::am` expected `PlaceHolderTypeWhichShouldNotExist`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "HashSet", Deserialization Error: Wrong type on `G::hs` expected `Unknown type`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArraySet", Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "HashMap", Deserialization Error: Wrong type on `G::hm` expected `Unknown type`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArrayMap", Deserialization Error: Wrong type on `G::am` expected `Unknown type`, got `null`
 # Src:
 <G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
 # Dst:
index dd26cc0..9c63bff 100644 (file)
 # Back in Nit:
 <E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
 
+Deserialization Error: Doesn't know how to deserialize class "Array"
+Deserialization Error: Wrong type on `E::a` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "Array"
+Deserialization Error: Wrong type on `E::b` expected `Unknown type`, got `null`
 # Nit:
 <E: 2222>
 
@@ -54,6 +58,7 @@
 # Back in Nit:
 null
 
+Deserialization Error: Doesn't know how to deserialize class "F"
 # Nit:
 <E: 33.33>
 
@@ -63,6 +68,7 @@ null
 # Back in Nit:
 null
 
+Deserialization Error: Doesn't know how to deserialize class "F"
 # Nit:
 <G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
 
@@ -72,3 +78,11 @@ null
 # Back in Nit:
 <G: hs: ; s: ; hm: ; am: >
 
+Deserialization Error: Doesn't know how to deserialize class "HashSet"
+Deserialization Error: Wrong type on `G::hs` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "ArraySet"
+Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "HashMap"
+Deserialization Error: Wrong type on `G::hm` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "ArrayMap"
+Deserialization Error: Wrong type on `G::am` expected `Unknown type`, got `null`
index b269caf..fe41ba6 100644 (file)
 # Back in Nit:
 <E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
 
+Deserialization Error: Doesn't know how to deserialize class "Array"
+Deserialization Error: Wrong type on `E::a` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "Array"
+Deserialization Error: Wrong type on `E::b` expected `Unknown type`, got `null`
 # Nit:
 <E: 2222>
 
 # Back in Nit:
 null
 
+Deserialization Error: Doesn't know how to deserialize class "F"
 # Nit:
 <E: 33.33>
 
@@ -143,6 +148,7 @@ null
 # Back in Nit:
 null
 
+Deserialization Error: Doesn't know how to deserialize class "F"
 # Nit:
 <G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
 
@@ -190,3 +196,11 @@ null
 # Back in Nit:
 <G: hs: ; s: ; hm: ; am: >
 
+Deserialization Error: Doesn't know how to deserialize class "HashSet"
+Deserialization Error: Wrong type on `G::hs` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "ArraySet"
+Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "HashMap"
+Deserialization Error: Wrong type on `G::hm` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "ArrayMap"
+Deserialization Error: Wrong type on `G::am` expected `Unknown type`, got `null`
index ce7118b..b079073 100644 (file)
@@ -1,6 +1,8 @@
 # This file is generated by nitserial
 # Do not modify, but you can redef
-module test_serialization_serial
+module test_serialization_serial is
+       no_warning("property-conflict")
+end
 
 import test_serialization
 import serialization
index c49a52a..3904578 100644 (file)
 }
 
 # Back in Nit:
-null
+<E: 2222>
 
 # Nit:
 <E: 33.33>
@@ -141,7 +141,7 @@ null
 }
 
 # Back in Nit:
-null
+<E: 33.33>
 
 # Nit:
 <G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
@@ -188,5 +188,5 @@ null
 }
 
 # Back in Nit:
-<G: hs: ; s: ; hm: ; am: >
+<G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
 
diff --git a/tests/sav/test_object_class_kind_alt1.res b/tests/sav/test_object_class_kind_alt1.res
new file mode 100644 (file)
index 0000000..3afdf0e
--- /dev/null
@@ -0,0 +1 @@
+alt/test_object_class_kind_alt1.nit:18,16--21: Error: `Object` must be an interface.
diff --git a/tests/sav/test_object_class_kind_alt2.res b/tests/sav/test_object_class_kind_alt2.res
new file mode 100644 (file)
index 0000000..9756e60
--- /dev/null
@@ -0,0 +1 @@
+alt/test_object_class_kind_alt2.nit:19,7--12: Error: `Object` must be an interface.
diff --git a/tests/sav/test_object_class_kind_alt3.res b/tests/sav/test_object_class_kind_alt3.res
new file mode 100644 (file)
index 0000000..2641394
--- /dev/null
@@ -0,0 +1 @@
+alt/test_object_class_kind_alt3.nit:20,6--11: Error: `Object` must be an interface.
diff --git a/tests/sav/test_object_class_kind_alt4.res b/tests/sav/test_object_class_kind_alt4.res
new file mode 100644 (file)
index 0000000..19f4896
--- /dev/null
@@ -0,0 +1 @@
+alt/test_object_class_kind_alt4.nit:21,14--19: Error: `Pointer` must be defined first.
index 155a39c..6b936ac 100644 (file)
@@ -15,6 +15,7 @@
 import test_deserialization
 import json::serialization
 #alt1# import test_deserialization_serial
+#alt3# import test_deserialization_serial
 
 var entities = new TestEntities
 
@@ -39,4 +40,5 @@ for o in tests do
        print "# Nit:\n{o}\n"
        print "# Json:\n{stream}\n"
        print "# Back in Nit:\n{deserialized or else "null"}\n"#alt2##alt4#
+       if deserializer.errors.not_empty then print deserializer.errors.join("\n")#alt2##alt4#
 end
diff --git a/tests/test_object_class_kind.nit b/tests/test_object_class_kind.nit
new file mode 100644 (file)
index 0000000..415694f
--- /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.
+
+import end
+
+interface Object #alt1-4#
+#alt1# abstract class Object
+#alt2# class Object
+#alt3# enum Object
+#alt4# extern class Object
+end