From: Jean Privat Date: Tue, 9 Aug 2016 01:18:27 +0000 (-0400) Subject: Merge: Faster lookup X-Git-Url: http://nitlanguage.org?hp=a2e474612b529a350e12df7daaeeab663a94250a Merge: Faster lookup 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 --- diff --git a/lib/gamnit/android_two_fingers_motion.nit b/lib/gamnit/android_two_fingers_motion.nit new file mode 100644 index 0000000..b8329ef --- /dev/null +++ b/lib/gamnit/android_two_fingers_motion.nit @@ -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 diff --git a/lib/gamnit/flat.nit b/lib/gamnit/flat.nit index 45d1ce9..8501ef4 100644 --- a/lib/gamnit/flat.nit +++ b/lib/gamnit/flat.nit @@ -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 diff --git a/lib/gen_nit.nit b/lib/gen_nit.nit index a9dc0ed..7af2044 100644 --- a/lib/gen_nit.nit +++ b/lib/gen_nit.nit @@ -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" diff --git a/lib/github/api.nit b/lib/github/api.nit index 59917c5..850bb6f 100644 --- a/lib/github/api.nit +++ b/lib/github/api.nit @@ -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 index 0000000..985f7aa --- /dev/null +++ b/lib/github/wallet.nit @@ -0,0 +1,180 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index 45578f2..6a66407 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -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 diff --git a/lib/mongodb/mongodb.nit b/lib/mongodb/mongodb.nit index ab4d387..80c4d4c 100644 --- a/lib/mongodb/mongodb.nit +++ b/lib/mongodb/mongodb.nit @@ -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`. diff --git a/lib/mongodb/native_mongodb.nit b/lib/mongodb/native_mongodb.nit index fb8b305..ba9b5e3 100644 --- a/lib/mongodb/native_mongodb.nit +++ b/lib/mongodb/native_mongodb.nit @@ -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 index 0000000..ca37876 --- /dev/null +++ b/lib/popcorn/pop_auth.nit @@ -0,0 +1,299 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 "

Hello {user.login}

" +# 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 index 0000000..1883b52 --- /dev/null +++ b/lib/popcorn/pop_config.nit @@ -0,0 +1,191 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 diff --git a/lib/popcorn/pop_handlers.nit b/lib/popcorn/pop_handlers.nit index 5d84961..1a22db6 100644 --- a/lib/popcorn/pop_handlers.nit +++ b/lib/popcorn/pop_handlers.nit @@ -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 diff --git a/lib/popcorn/pop_middlewares.nit b/lib/popcorn/pop_middlewares.nit index 518d9dc..bb25da0 100644 --- a/lib/popcorn/pop_middlewares.nit +++ b/lib/popcorn/pop_middlewares.nit @@ -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 index 0000000..9f9cfed --- /dev/null +++ b/lib/popcorn/pop_repos.nit @@ -0,0 +1,317 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 index 0000000..386bb24 --- /dev/null +++ b/lib/popcorn/pop_validation.nit @@ -0,0 +1,703 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 diff --git a/lib/serialization/serialization.nit b/lib/serialization/serialization.nit index 8ae8d2f..0ad3019 100644 --- a/lib/serialization/serialization.nit +++ b/lib/serialization/serialization.nit @@ -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" diff --git a/src/frontend/serialization_phase.nit b/src/frontend/serialization_phase.nit index 8b3de28..bb5cb8d 100644 --- a/src/frontend/serialization_phase.nit +++ b/src/frontend/serialization_phase.nit @@ -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 diff --git a/src/metrics/mendel_metrics.nit b/src/metrics/mendel_metrics.nit index 1eb4e5b..5bd6f97 100644 --- a/src/metrics/mendel_metrics.nit +++ b/src/metrics/mendel_metrics.nit @@ -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 diff --git a/src/modelize/modelize_class.nit b/src/modelize/modelize_class.nit index 045e829..8164668 100644 --- a/src/modelize/modelize_class.nit +++ b/src/modelize/modelize_class.nit @@ -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 diff --git a/src/nitserial.nit b/src/nitserial.nit index 9c8a07b..f7481d4 100644 --- a/src/nitserial.nit +++ b/src/nitserial.nit @@ -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 diff --git a/tests/sav/nitce/test_binary_deserialization_alt1.res b/tests/sav/nitce/test_binary_deserialization_alt1.res index d99f2e7..730ae5c 100644 --- a/tests/sav/nitce/test_binary_deserialization_alt1.res +++ b/tests/sav/nitce/test_binary_deserialization_alt1.res @@ -20,7 +20,7 @@ <- false p4ssw0rd> 1111 f" \/> 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: # 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: # Dst: diff --git a/tests/sav/nitce/test_json_deserialization_alt1.res b/tests/sav/nitce/test_json_deserialization_alt1.res index dd26cc0..9c63bff 100644 --- a/tests/sav/nitce/test_json_deserialization_alt1.res +++ b/tests/sav/nitce/test_json_deserialization_alt1.res @@ -45,6 +45,10 @@ # Back in Nit: +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: @@ -54,6 +58,7 @@ # Back in Nit: null +Deserialization Error: Doesn't know how to deserialize class "F" # Nit: @@ -63,6 +68,7 @@ null # Back in Nit: null +Deserialization Error: Doesn't know how to deserialize class "F" # Nit: @@ -72,3 +78,11 @@ null # Back in Nit: +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` diff --git a/tests/sav/nitce/test_json_deserialization_alt3.res b/tests/sav/nitce/test_json_deserialization_alt3.res index b269caf..fe41ba6 100644 --- a/tests/sav/nitce/test_json_deserialization_alt3.res +++ b/tests/sav/nitce/test_json_deserialization_alt3.res @@ -119,6 +119,10 @@ # Back in Nit: +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: @@ -131,6 +135,7 @@ # Back in Nit: null +Deserialization Error: Doesn't know how to deserialize class "F" # Nit: @@ -143,6 +148,7 @@ null # Back in Nit: null +Deserialization Error: Doesn't know how to deserialize class "F" # Nit: @@ -190,3 +196,11 @@ null # Back in Nit: +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` diff --git a/tests/sav/nitserial_args1.res b/tests/sav/nitserial_args1.res index ce7118b..b079073 100644 --- a/tests/sav/nitserial_args1.res +++ b/tests/sav/nitserial_args1.res @@ -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 diff --git a/tests/sav/test_json_deserialization_alt3.res b/tests/sav/test_json_deserialization_alt3.res index c49a52a..3904578 100644 --- a/tests/sav/test_json_deserialization_alt3.res +++ b/tests/sav/test_json_deserialization_alt3.res @@ -129,7 +129,7 @@ } # Back in Nit: -null + # Nit: @@ -141,7 +141,7 @@ null } # Back in Nit: -null + # Nit: @@ -188,5 +188,5 @@ null } # Back in Nit: - + diff --git a/tests/sav/test_object_class_kind_alt1.res b/tests/sav/test_object_class_kind_alt1.res new file mode 100644 index 0000000..3afdf0e --- /dev/null +++ b/tests/sav/test_object_class_kind_alt1.res @@ -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 index 0000000..9756e60 --- /dev/null +++ b/tests/sav/test_object_class_kind_alt2.res @@ -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 index 0000000..2641394 --- /dev/null +++ b/tests/sav/test_object_class_kind_alt3.res @@ -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 index 0000000..19f4896 --- /dev/null +++ b/tests/sav/test_object_class_kind_alt4.res @@ -0,0 +1 @@ +alt/test_object_class_kind_alt4.nit:21,14--19: Error: `Pointer` must be defined first. diff --git a/tests/test_json_deserialization.nit b/tests/test_json_deserialization.nit index 155a39c..6b936ac 100644 --- a/tests/test_json_deserialization.nit +++ b/tests/test_json_deserialization.nit @@ -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 index 0000000..415694f --- /dev/null +++ b/tests/test_object_class_kind.nit @@ -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