--- /dev/null
+# 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
import gamnit::cameras
import gamnit::limit_fps
+import android_two_fingers_motion is conditional(android)
+
# Image to draw on screen
class Sprite
# 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]
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"
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.
--- /dev/null
+# 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
# 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
v.stream.write """","""
v.new_line_and_indent
v.stream.write """"__items": """
+
+ core_serialize_to v
end
serialize_to_pure_json v
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
# 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
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
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)
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
# 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
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.
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`.
# * 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
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.
--- /dev/null
+# 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
--- /dev/null
+# 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
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
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
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
--- /dev/null
+# 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
--- /dev/null
+# 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
class AttributeTypeError
super Error
+ autoinit receiver, attribute_name, attribute, expected_type
+
# Parent object of the problematic attribute
var receiver: Object
# 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"
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
# 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:
#
# * 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
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)
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
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
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
<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:
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:
# 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>
# 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>
# 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`
# 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>
# 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>
# 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`
# 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
}
# Back in Nit:
-null
+<E: 2222>
# Nit:
<E: 33.33>
}
# 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>
}
# 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>
--- /dev/null
+alt/test_object_class_kind_alt1.nit:18,16--21: Error: `Object` must be an interface.
--- /dev/null
+alt/test_object_class_kind_alt2.nit:19,7--12: Error: `Object` must be an interface.
--- /dev/null
+alt/test_object_class_kind_alt3.nit:20,6--11: Error: `Object` must be an interface.
--- /dev/null
+alt/test_object_class_kind_alt4.nit:21,14--19: Error: `Pointer` must be defined first.
import test_deserialization
import json::serialization
#alt1# import test_deserialization_serial
+#alt3# import test_deserialization_serial
var entities = new TestEntities
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
--- /dev/null
+# 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