Merge: Implementing the Nit wrapper over the native postgres wrapper
authorJean Privat <jean@pryen.org>
Tue, 24 May 2016 13:16:42 +0000 (09:16 -0400)
committerJean Privat <jean@pryen.org>
Tue, 24 May 2016 13:16:42 +0000 (09:16 -0400)
This wrapper works around the `native_postgres.nit` class and implements the minimal amount of functionality of that class to start a connection, use the execution methods, and inspect the results. It's the next phase in the postgres package, I've also added tests.

@xymus

Pull-Request: #2104
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

187 files changed:
.gitattributes
.gitignore
contrib/asteronits/Makefile
contrib/benitlux/.gitignore
contrib/benitlux/Makefile
contrib/benitlux/README.md
contrib/benitlux/android/res/.gitignore [new file with mode: 0644]
contrib/benitlux/android/res/values/styles.xml [new file with mode: 0644]
contrib/benitlux/art/icon.svg [new file with mode: 0644]
contrib/benitlux/art/notif.svg [new file with mode: 0644]
contrib/benitlux/ios/.gitignore [new file with mode: 0644]
contrib/benitlux/net.xymus.benitlux.txt [new file with mode: 0644]
contrib/benitlux/package.ini
contrib/benitlux/src/client/android.nit [new file with mode: 0644]
contrib/benitlux/src/client/android_proto.nit [new file with mode: 0644]
contrib/benitlux/src/client/base.nit [new file with mode: 0644]
contrib/benitlux/src/client/client.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/checkins.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/debug.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/push.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/translations.nit [new file with mode: 0644]
contrib/benitlux/src/client/ios.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/beer_views.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/home_views.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/social_views.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/user_views.nit [new file with mode: 0644]
contrib/benitlux/src/server/benitlux_controller.nit [moved from contrib/benitlux/src/benitlux_controller.nit with 100% similarity]
contrib/benitlux/src/server/benitlux_daily.nit [moved from contrib/benitlux/src/benitlux_daily.nit with 100% similarity]
contrib/benitlux/src/server/benitlux_db.nit [moved from contrib/benitlux/src/benitlux_db.nit with 100% similarity]
contrib/benitlux/src/server/benitlux_social.nit [moved from contrib/benitlux/src/benitlux_social.nit with 100% similarity]
contrib/benitlux/src/server/benitlux_view.nit [moved from contrib/benitlux/src/benitlux_view.nit with 100% similarity]
contrib/benitlux/src/server/server.nit [moved from contrib/benitlux/src/benitlux_web.nit with 98% similarity]
contrib/tinks/src/client/tinks_vr.nit [new file with mode: 0644]
contrib/tnitter/package.ini
contrib/tnitter/src/tnitter_app.nit
contrib/xymus_net/.gitignore [new file with mode: 0644]
contrib/xymus_net/Makefile [new file with mode: 0644]
contrib/xymus_net/README.md [new file with mode: 0644]
contrib/xymus_net/package.ini [new file with mode: 0644]
contrib/xymus_net/xymus_net.nit [moved from lib/nitcorn/examples/src/xymus_net.nit with 96% similarity]
examples/calculator/package.ini
examples/calculator/src/calculator.nit
examples/rosettacode/perlin_noise.nit
lib/a_star.nit
lib/android/NitActivity.java
lib/android/input_events.nit
lib/android/key_event.nit [new file with mode: 0644]
lib/android/nit_activity.nit
lib/android/ui/ui.nit
lib/app/ui.nit
lib/bucketed_game.nit
lib/core/text/flat.nit
lib/gamnit/cameras.nit
lib/gamnit/depth/depth_core.nit
lib/gamnit/depth/more_materials.nit
lib/gamnit/depth/more_meshes.nit
lib/gamnit/depth/more_models.nit
lib/gamnit/display.nit
lib/gamnit/display_android.nit
lib/gamnit/display_linux.nit
lib/gamnit/egl.nit
lib/gamnit/limit_fps.nit
lib/gamnit/programs.nit
lib/gamnit/texture_atlas_parser.nit [moved from contrib/asteronits/src/texture_atlas_parser.nit with 98% similarity]
lib/gamnit/textures.nit
lib/geometry/polygon.nit
lib/ios/ui/ui.nit
lib/linux/ui.nit
lib/nitcorn/README.md
lib/nitcorn/examples/Makefile
lib/nitcorn/file_server.nit
lib/nitcorn/media_types.nit
lib/nitcorn/sessions.nit
lib/nitcorn/vararg_routes.nit
lib/noise.nit
lib/popcorn/.gitignore [new file with mode: 0644]
lib/popcorn/Makefile [new file with mode: 0644]
lib/popcorn/README.md [new file with mode: 0644]
lib/popcorn/examples/angular/example_angular.nit [new file with mode: 0644]
lib/popcorn/examples/angular/www/index.html [new file with mode: 0644]
lib/popcorn/examples/angular/www/javascripts/ng-example.js [new file with mode: 0644]
lib/popcorn/examples/angular/www/views/index.html [new file with mode: 0644]
lib/popcorn/examples/handlers/example_post_handler.nit [new file with mode: 0644]
lib/popcorn/examples/handlers/example_query_string.nit [new file with mode: 0644]
lib/popcorn/examples/hello_world/example_hello.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_advanced_logger.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_html_error_handler.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_simple_error_handler.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_simple_logger.nit [new file with mode: 0644]
lib/popcorn/examples/mongodb/example_mongodb.nit [new file with mode: 0644]
lib/popcorn/examples/routing/example_glob_route.nit [new file with mode: 0644]
lib/popcorn/examples/routing/example_param_route.nit [new file with mode: 0644]
lib/popcorn/examples/routing/example_router.nit [new file with mode: 0644]
lib/popcorn/examples/sessions/example_session.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/example_static.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/example_static_multiple.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/files/index.html [new file with mode: 0644]
lib/popcorn/examples/static_files/public/css/style.css [new file with mode: 0644]
lib/popcorn/examples/static_files/public/hello.html [new file with mode: 0644]
lib/popcorn/examples/static_files/public/images/trollface.jpg [new file with mode: 0644]
lib/popcorn/examples/static_files/public/js/app.js [new file with mode: 0644]
lib/popcorn/package.ini [new file with mode: 0644]
lib/popcorn/pop_handlers.nit [new file with mode: 0644]
lib/popcorn/pop_middlewares.nit [new file with mode: 0644]
lib/popcorn/pop_routes.nit [new file with mode: 0644]
lib/popcorn/popcorn.nit [new file with mode: 0644]
lib/popcorn/test_pop_routes.nit [new file with mode: 0644]
lib/popcorn/tests/Makefile [new file with mode: 0644]
lib/popcorn/tests/base_tests.nit [new file with mode: 0644]
lib/popcorn/tests/res/test_example_advanced_logger.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_angular.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_glob_route.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_hello.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_html_error_handler.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_param_route.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_post.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_query_string.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_router.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_session.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_simple_error_handler.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_simple_logger.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_static.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_static_multiple.res [new file with mode: 0644]
lib/popcorn/tests/res/test_router.res [new file with mode: 0644]
lib/popcorn/tests/res/test_routes.res [new file with mode: 0644]
lib/popcorn/tests/test_example_advanced_logger.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_angular.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_glob_route.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_hello.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_html_error_handler.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_param_route.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_post.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_query_string.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_router.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_session.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_simple_error_handler.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_simple_logger.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_static.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_static_multiple.nit [new file with mode: 0644]
lib/popcorn/tests/test_router.nit [new file with mode: 0644]
lib/popcorn/tests/test_routes.nit [new file with mode: 0644]
lib/popcorn/tests/tests.sh [new file with mode: 0755]
lib/serialization/caching.nit
lib/template/macro.nit
misc/docker/Dockerfile
misc/docker/README.md [new file with mode: 0644]
misc/docker/full/Dockerfile [new file with mode: 0644]
misc/docker/hello/Dockerfile [new file with mode: 0644]
misc/docker/hello/src/hello.nit [new file with mode: 0644]
share/man/nitunit.md
src/catalog.nit
src/model/model_collect.nit
src/model/model_json.nit [new file with mode: 0644]
src/model/model_views.nit
src/modelize/modelize_property.nit
src/nitcatalog.nit
src/nitunit.nit
src/nitweb.nit
src/parser/README.md
src/platform/app_annotations.nit
src/testing/testing_base.nit
src/testing/testing_doc.nit
src/testing/testing_suite.nit
src/web/model_html.nit
src/web/web_actions.nit
src/web/web_base.nit
src/web/web_views.nit
tests/base_redef.nit [new file with mode: 0644]
tests/listfull.sh
tests/sav/base_redef.res [new file with mode: 0644]
tests/sav/base_redef_alt1.res [new file with mode: 0644]
tests/sav/base_redef_alt2.res [new file with mode: 0644]
tests/sav/nitcatalog_args1.res
tests/sav/nitunit_args1.res
tests/sav/nitunit_args4.res
tests/sav/nitunit_args5.res
tests/sav/nitunit_args6.res
tests/sav/nitunit_args7.res
tests/sav/nitunit_args8.res
tests/sav/nitunit_args9.res
tests/sav/test_substring.res
tests/sav/xymus_net.res [deleted file]
tests/test_nitunit4/test_nitunit4.nit
tests/test_nitunit4/test_nitunit4.sav/test_bar.res [new file with mode: 0644]
tests/test_nitunit4/test_nitunit4.sav/test_baz.res [new file with mode: 0644]
tests/test_nitunit4/test_nitunit4_base.nit
tests/test_substring.nit

index ca8864b..02c0677 100644 (file)
@@ -6,4 +6,5 @@ tables_nit.c            -diff
 c_src/**               -diff
 
 tests/sav/**/*.res     -whitespace
+lib/popcorn/tests/res/*.res -whitespace
 *.patch                        -whitespace
index 80ef46d..a739d3f 100644 (file)
@@ -1,5 +1,6 @@
 *.bak
 *.swp
+*.swo
 *~
 .project
 EIFGENs
index f110c83..a5e910a 100644 (file)
@@ -6,17 +6,17 @@ all: bin/asteronits
 bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit linux) ${NITC} pre-build
        ${NITC} src/asteronits.nit -m linux -o $@
 
-bin/texture_atlas_parser: src/texture_atlas_parser.nit
-       ${NITC} src/texture_atlas_parser.nit -o $@
+bin/texture_atlas_parser: ../../lib/gamnit/texture_atlas_parser.nit
+       ${NITC} ../../lib/gamnit/texture_atlas_parser.nit -o $@
 
 src/controls.nit: art/controls.svg
        make -C ../inkscape_tools/
        ../inkscape_tools/bin/svg_to_png_and_nit art/controls.svg -a assets/ -s src/ -x 2.0 -g
 
-src/spritesheet_city.nit: bin/texture_atlas_parser
+src/spritesheet.nit: bin/texture_atlas_parser
        bin/texture_atlas_parser art/sheet.xml --dir src/ -n spritesheet
 
-pre-build: src/controls.nit src/spritesheet_city.nit
+pre-build: src/controls.nit src/spritesheet.nit
 
 check: bin/asteronits
        NIT_TESTING=true bin/asteronits
index 562fb6d..ccbe098 100644 (file)
@@ -1,4 +1,4 @@
-src/benitlux_restful.nit
+src/server/benitlux_restful.nit
 *.db
 *.email
 benitlux_corrections.txt
index c14d648..2142041 100644 (file)
@@ -1,19 +1,19 @@
 SERVER ?= localhost:8080
 
-all: server
+all: server bin/report bin/benitlux
 
 server: bin/benitlux_daily bin/benitlux_web
-bin/benitlux_daily: $(shell ../../bin/nitls -M src/benitlux_daily.nit)
+bin/benitlux_daily: $(shell ../../bin/nitls -M src/server/benitlux_daily.nit)
        mkdir -p bin/
-       ../../bin/nitc -o $@ src/benitlux_daily.nit
+       ../../bin/nitc -o $@ src/server/benitlux_daily.nit
 
-bin/benitlux_web: $(shell ../../bin/nitls -M src/benitlux_web.nit) src/benitlux_restful.nit
+bin/benitlux_web: $(shell ../../bin/nitls -M src/server/server.nit) src/server/benitlux_restful.nit
        mkdir -p bin/
-       ../../bin/nitc -o $@ src/benitlux_web.nit -D iface=$(SERVER)
+       ../../bin/nitc -o $@ src/server/server.nit -D iface=$(SERVER)
 
-pre-build: src/benitlux_restful.nit
-src/benitlux_restful.nit: $(shell ../../bin/nitls -M src/benitlux_controller.nit)
-       ../../bin/nitrestful -o $@ src/benitlux_controller.nit
+pre-build: src/server/benitlux_restful.nit
+src/server/benitlux_restful.nit: $(shell ../../bin/nitls -M src/server/benitlux_controller.nit)
+       ../../bin/nitrestful -o $@ src/server/benitlux_controller.nit
 
 # ---
 # Report
@@ -23,3 +23,64 @@ bin/report: $(shell ../../bin/nitls -M src/report.nit)
 
 report: bin/report
        bin/report
+
+# ---
+# GTK+ client
+
+bin/benitlux: $(shell ../../bin/nitls -M src/client/client.nit)
+       mkdir -p bin/
+       ../../bin/nitc -o bin/benitlux src/client/client.nit -m linux -D benitlux_rest_server_uri=http://$(SERVER)/
+
+# ---
+# Android
+
+# Main icon
+android/res/drawable-hdpi/icon.png:
+       ../inkscape_tools/bin/svg_to_icons art/icon.svg --android --out android/res/
+
+# Notification icon, white only
+android/res/drawable-hdpi/notif.png:
+       ../inkscape_tools/bin/svg_to_icons art/notif.svg --android --out android/res/ --name notif
+
+android-res: android/res/drawable-hdpi/icon.png android/res/drawable-hdpi/notif.png
+
+# Dev / debug app
+android: bin/benitlux.apk
+bin/benitlux.apk: $(shell ../../bin/nitls -M src/client/android.nit) android-res
+       mkdir -p bin/ res/
+       ../../bin/nitc -o $@ src/client/android.nit -m src/client/features/debug.nit \
+               -D benitlux_rest_server_uri=http://$(SERVER)/
+
+# Pure portable prototype, for comparison
+bin/proto.apk: $(shell ../../bin/nitls -M src/client/android_proto.nit) android-res
+       mkdir -p bin/ res/
+       ../../bin/nitc -o $@ src/client/android_proto.nit \
+               -D benitlux_rest_server_uri=http://$(SERVER)/
+
+# Release version
+android-release: $(shell ../../bin/nitls -M src/client/android.nit) android-res
+       mkdir -p bin/ res/
+       ../../bin/nitc -o bin/benitlux.apk src/client/android.nit \
+               -D benitlux_rest_server_uri=http://xymus.net/benitlux/ --release
+
+# ---
+# iOS
+
+ios: bin/benitlux.app
+bin/benitlux.app: $(shell ../../bin/nitls -M src/client/ios.nit) ios/AppIcon.appiconset/Contents.json
+       mkdir -p bin/
+       rm -rf bin/benitlux.app/
+       ../../bin/nitc -o bin/benitlux.app src/client/ios.nit -D benitlux_rest_server_uri=http://$(SERVER)/
+
+bin/proto.app: $(shell ../../bin/nitls -M src/client/ios_proto.nit) ios/AppIcon.appiconset/Contents.json
+       mkdir -p bin/ res/
+       ../../bin/nitc -o $@ src/client/ios_proto.nit \
+               -D benitlux_rest_server_uri=http://$(SERVER)/
+
+ios-release: $(shell ../../bin/nitls -M src/client/ios.nit) ios/AppIcon.appiconset/Contents.json
+       mkdir -p bin/
+       ../../bin/nitc -o bin/benitlux.app src/client/ios.nit -D benitlux_rest_server_uri=http://$(SERVER)/
+
+ios/AppIcon.appiconset/Contents.json: art/icon.svg
+       mkdir -p ios
+       ../inkscape_tools/bin/svg_to_icons art/icon.svg --ios --out ios/AppIcon.appiconset/
index 6f4ce46..b8aed58 100644 (file)
@@ -1,21 +1,45 @@
-An unofficial mailing list and other tools to keep faithful bargoers informed of the beers available at the excellent Brasserie Bénélux.
+An unofficial app and mailing list to keep faithful bargoers informed of the beers available at the excellent Brasserie Bénélux.
 
-This project is composed of two softwares:
+This project is composed of three softwares:
 
-* a Web interface to subscribe and unsubscribe,
-* and a daily background program which updates the BD and send emails.
+* A mobile app and social network,
+* a server with a RESTful API for the mobile app and a web interface to subscribe to the mailing list
+* and a daily background program which updates the DB and send emails.
 
-The web interface is currently published at <http://benitlux.xymus.net/>
+The mobile app is available on the Nit F-Droid repository, see http://nitlanguage.org/fdroid.
+The web interface is currently published at http://benitlux.xymus.net.
 
 # Compile and execute
 
-Make sure all the required packages are installed. Under Debian or Ubuntu, you can use: `apt-get install libevent-dev libsqlite3-dev libcurl4-gnutls-dev sendmail`
+First, choose a server and set the `SERVER` environment variable accordingly.
+It can be localhost, a local development server or the official server.
 
-To compile, run: `make`
+* `SERVER` defaults to `localhost:8080`.
+  This is enough to test running the server and the GNU/Linux client on the same machine.
 
-To launch the daily background program, run: `bin/benitlux_daily` (the argument `-e` activates sending emails)
+* Set `SERVER=192.168.0.1` or to your IP to quickly setup a development server.
+  This allows you to work and test both the clients and the server.
 
-To launch the Web interface, run: `bin/benitlux_web`
+* Set `SERVER=benitlux.xymus.net` to use the official server, it should work with all clients.
+  It is not advised to use the official server with unstable clients.
+
+## Mobile client
+
+Build and run on GNU/Linux with `make bin/benitlux && bin/benitlux`
+
+Build and install for Android with: `make bin/benitlux.apk && adb install -rd bin/benitlux.apk`
+
+Build and simulate for iOS with: `make bin/benitlux.app && ios-sim launch bin/benitlux.app`
+
+## Server
+
+Install all required development packages. Under Debian or Ubuntu, you can use: `apt-get install libevent-dev libsqlite3-dev libcurl4-gnutls-dev sendmail`
+
+Compile with: `make`
+
+Launch the daily background program with: `bin/benitlux_daily` (the argument `-e` sends the emails)
+
+Launch the server with: `bin/benitlux_web`
 
 The Web interface will be accessible at <http://localhost:8080/>
 
@@ -25,10 +49,9 @@ The Web interface will be accessible at <http://localhost:8080/>
 - [x] Daily mailer
 - [x] Web interface
 - [x] Serialization and deserialization of data classes
-- [ ] Android app
-- [ ] iOS app
+- [x] Android app
+- [x] iOS app
 - [ ] Charlevoix location support
-- [ ] Customize mails (daily, on change, per locations)
 - [ ] Authenticate unsubscribe actions over GET
-- [ ] Social network and location updates
+- [x] Social network and location updates
 - [ ] Event updates
diff --git a/contrib/benitlux/android/res/.gitignore b/contrib/benitlux/android/res/.gitignore
new file mode 100644 (file)
index 0000000..46ce728
--- /dev/null
@@ -0,0 +1 @@
+drawable*
diff --git a/contrib/benitlux/android/res/values/styles.xml b/contrib/benitlux/android/res/values/styles.xml
new file mode 100644 (file)
index 0000000..c435c5f
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+       <color name="item_background">#000000</color>
+</resources>
diff --git a/contrib/benitlux/art/icon.svg b/contrib/benitlux/art/icon.svg
new file mode 100644 (file)
index 0000000..779a3e3
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="512"
+   height="512"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="icon.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="239.85532"
+     inkscape:cy="187.76324"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1196"
+     inkscape:window-height="1109"
+     inkscape:window-x="2732"
+     inkscape:window-y="1283"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-540.36218)">
+    <path
+       sodipodi:type="arc"
+       style="fill:#000000;fill-opacity:1;stroke:none"
+       id="path2986"
+       sodipodi:cx="270.72089"
+       sodipodi:cy="251.38065"
+       sodipodi:rx="241.42645"
+       sodipodi:ry="241.42645"
+       d="m 512.14734,251.38065 a 241.42645,241.42645 0 1 1 -482.852906,0 241.42645,241.42645 0 1 1 482.852906,0 z"
+       transform="matrix(1.0603643,0,0,1.0603643,-31.062773,529.80711)" />
+    <text
+       xml:space="preserve"
+       style="font-size:394.38067627px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Sans"
+       x="120.70995"
+       y="938.47345"
+       id="text2987"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2989"
+         x="120.70995"
+         y="938.47345"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Webdings;-inkscape-font-specification:Webdings Bold">B</tspan></text>
+    <path
+       transform="matrix(0.96890975,0,0,0.96890975,-6.3041178,552.79701)"
+       d="m 512.14734,251.38065 a 241.42645,241.42645 0 1 1 -482.852906,0 241.42645,241.42645 0 1 1 482.852906,0 z"
+       sodipodi:ry="241.42645"
+       sodipodi:rx="241.42645"
+       sodipodi:cy="251.38065"
+       sodipodi:cx="270.72089"
+       id="path2988"
+       style="fill:none;stroke:#ffffff;stroke-width:10.02402973;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       sodipodi:type="arc" />
+  </g>
+</svg>
diff --git a/contrib/benitlux/art/notif.svg b/contrib/benitlux/art/notif.svg
new file mode 100644 (file)
index 0000000..871b01c
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="512"
+   height="512"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.5 r10040"
+   sodipodi:docname="icon.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="331.40838"
+     inkscape:cy="413.97896"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1196"
+     inkscape:window-height="1109"
+     inkscape:window-x="2732"
+     inkscape:window-y="297"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-540.36218)">
+    <text
+       xml:space="preserve"
+       style="font-size:419.40737915px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Sans"
+       x="112.12482"
+       y="947.49194"
+       id="text2987"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2989"
+         x="112.12482"
+         y="947.49194"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Webdings;-inkscape-font-specification:Webdings Bold">B</tspan></text>
+    <path
+       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10.02402973;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+       d="M 256 2.0625 C 115.82007 2.0625 2.0625 115.82007 2.0625 256 C 2.0625 396.17993 115.82007 509.9375 256 509.9375 C 396.17993 509.9375 509.9375 396.17993 509.9375 256 C 509.9375 115.82007 396.17993 2.0625 256 2.0625 z M 256 37.0625 C 376.96769 37.0625 474.9375 135.03232 474.9375 256 C 474.9375 376.96769 376.96769 474.90625 256 474.90625 C 135.03232 474.90625 37.0625 376.96769 37.0625 256 C 37.0625 135.03232 135.03232 37.0625 256 37.0625 z "
+       transform="translate(0,540.36218)"
+       id="path2988" />
+  </g>
+</svg>
diff --git a/contrib/benitlux/ios/.gitignore b/contrib/benitlux/ios/.gitignore
new file mode 100644 (file)
index 0000000..72e8ffc
--- /dev/null
@@ -0,0 +1 @@
+*
diff --git a/contrib/benitlux/net.xymus.benitlux.txt b/contrib/benitlux/net.xymus.benitlux.txt
new file mode 100644 (file)
index 0000000..fa717a9
--- /dev/null
@@ -0,0 +1,11 @@
+Categories:Nit,Internet
+License:Apache2
+Web Site:http://xymus.net/benitlux
+Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/contrib/benitlux
+Issue Tracker:https://github.com/nitlang/nit/issues
+
+Summary:Mobile client for the Benitlux social network
+Description:
+View the beer menu, rate beers, view community rating, and receive notifications
+of the daily menu changes and when friends are on location.
+.
index e53853c..38eeee8 100644 (file)
@@ -1,12 +1,13 @@
 [package]
 name=benitlux
-tags=network
+tags=mobile,web
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
 browse=https://github.com/nitlang/nit/tree/master/contrib/benitlux/
 git=https://github.com/nitlang/nit.git
 git.directory=contrib/benitlux/
-homepage=http://nitlanguage.org
+homepage=http://xymus.net/benitlux/
 issues=https://github.com/nitlang/nit/issues
-tryit=http://benitlux.xymus.net/
+tryit=http://xymus.net/benitlux/
+apk=http://nitlanguage.org/fdroid/apk/tnitter.apk
diff --git a/contrib/benitlux/src/client/android.nit b/contrib/benitlux/src/client/android.nit
new file mode 100644 (file)
index 0000000..f3391bf
--- /dev/null
@@ -0,0 +1,268 @@
+# 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.
+
+# Android variant improved with platform specific services
+module android is
+       android_manifest_activity """android:theme="@android:style/Theme.DeviceDefault" """
+       android_api_min 16 # For BigTextStyle
+       android_api_target 16
+end
+
+import ::android::portrait
+import ::android::toast
+import ::android::wifi
+import ::android::service::at_boot
+
+import client
+import push
+import checkins
+
+redef class App
+
+       redef fun on_create
+       do
+               super
+
+               # Launch service with app, if it wasn't already launched at boot
+               start_service
+       end
+
+       # Use Android toasts if there is an activity, otherwise fallback on the log
+       redef fun feedback(text)
+       do
+               if activities.not_empty then
+                       app.toast(text.to_s, false)
+               else super
+       end
+
+       # Register to callback `async_wifi_scan_available` when a wifi scan is available
+       private fun notify_on_wifi_scan(context: NativeContext)
+       import async_wifi_scan_available in "Java" `{
+
+               android.content.IntentFilter filter = new android.content.IntentFilter();
+               filter.addAction(android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+               final int final_self = self;
+
+               context.registerReceiver(
+                       new android.content.BroadcastReceiver() {
+                               @Override
+                               public void onReceive(android.content.Context context, android.content.Intent intent) {
+                                       if (intent.getAction().equals(android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                                               App_async_wifi_scan_available(final_self);
+                                       }
+                               }
+                       }, filter);
+       `}
+
+       private fun async_wifi_scan_available do run_on_ui_thread task_on_wifi_scan_available
+
+       private var task_on_wifi_scan_available = new WifiScanAvailable is lazy
+end
+
+redef class Service
+       redef fun on_start_command(intent, flags, id)
+       do
+               app.notify_on_wifi_scan native
+
+               # Check token validity
+               (new PushHttpRequest("push/check_token?token={app.token}")).start
+
+               return start_sticky
+       end
+end
+
+# Task ran on the UI thread when a wifi scan is available
+private class WifiScanAvailable
+       super Task
+
+       redef fun main
+       do
+               jni_env.push_local_frame 4
+               var manager = app.native_context.wifi_manager
+               var networks = manager.get_scan_results
+               var found_ben = false
+               for i in networks.length.times do
+                       jni_env.push_local_frame 4
+                       var net = networks[i]
+                       var ssid = net.ssid.to_s
+
+                       # TODO use BSSID instead
+                       #var bssid = net.bssid.to_s
+                       var target_ssids = ["Benelux"]
+                       if target_ssids.has(ssid) then # and bssid == "C8:F7:33:81:B0:E6" then
+                               found_ben = true
+                               break
+                       end
+                       jni_env.pop_local_frame
+               end
+               jni_env.pop_local_frame
+
+               if found_ben then
+                       app.on_check_in
+               else app.on_check_out
+       end
+end
+
+redef class SectionTitle
+       init do set_text_style(native, app.native_context)
+
+       private fun set_text_style(view: NativeTextView, context: NativeContext) in "Java" `{
+               view.setTextAppearance(context, android.R.style.TextAppearance_Large);
+       `}
+end
+
+redef class ItemView
+       init do set_backgroud(native, app.native_context)
+
+       private fun set_backgroud(view: NativeView, context: NativeContext) in "Java" `{
+               int color = context.getResources().getIdentifier("item_background", "color", context.getPackageName());
+               view.setBackgroundResource(color);
+       `}
+end
+
+# Use Android notifications
+redef fun notify(title, content, id)
+do
+       var service = app.service
+       assert service != null
+       native_notify(service.native, id, title.to_java_string, content.to_java_string)
+end
+
+private fun native_notify(context: NativeService, id: Int, title, content: JavaString)
+in "Java" `{
+       int icon = context.getResources().getIdentifier(
+               "notif", "drawable", context.getPackageName());
+
+       android.app.Notification.BigTextStyle style =
+               new android.app.Notification.BigTextStyle();
+       style.bigText(content);
+
+       android.content.Intent intent = new android.content.Intent(
+               context, nit.app.NitActivity.class);
+       android.app.PendingIntent pendingIntent = android.app.PendingIntent.getActivity(
+               context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);
+
+       android.app.Notification notif = new android.app.Notification.Builder(context)
+               .setContentTitle(title)
+               .setContentText(content)
+               .setSmallIcon(icon)
+               .setAutoCancel(true)
+               .setOngoing(false)
+               .setStyle(style)
+               .setContentIntent(pendingIntent)
+               .setDefaults(android.app.Notification.DEFAULT_SOUND |
+                            android.app.Notification.DEFAULT_LIGHTS)
+               .build();
+
+       android.app.NotificationManager notificationManager =
+         (android.app.NotificationManager)context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
+
+       notificationManager.notify((int)id, notif);
+`}
+
+
+# Use `RatingBar` as the beer rating control
+redef class BeerView
+       redef fun setup_stars(rating)
+       do
+               var title = "Review %0".t.format(beer_info.beer.name).to_java_string
+               native_setup_stars(app.native_context, top_line_layout.native, rating, title, app.user != null)
+       end
+
+       private fun native_setup_stars(context: NativeContext, layout: NativeViewGroup, rating: Int, title: JavaString, loggedin: Bool)
+       import on_review in "Java" `{
+               // Set an indicator/non-interactive display
+               final android.widget.RatingBar view = new android.widget.RatingBar(
+                       context, null, android.R.attr.ratingBarStyleIndicator);
+               view.setNumStars(5);
+               view.setRating(rating);
+               view.setIsIndicator(true);
+
+               final android.view.ViewGroup.MarginLayoutParams params = new android.view.ViewGroup.MarginLayoutParams(
+                       android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
+                       android.widget.LinearLayout.LayoutParams.FILL_PARENT);
+               layout.addView(view, params);
+
+               // Make some variables final to used in anonymous class and delayed methods
+               final android.content.Context final_context = context;
+               final long final_rating = rating;
+               final String final_title = title;
+               final boolean final_loggedin = loggedin;
+
+               final int final_self = self;
+               BeerView_incr_ref(self); // Nit GC
+
+               view.setOnTouchListener(new android.view.View.OnTouchListener() {
+                       @Override
+                       public boolean onTouch(android.view.View v, android.view.MotionEvent event) {
+                               if (event.getAction() != android.view.MotionEvent.ACTION_UP) return true;
+
+                               // Don't show dialog if not logged in
+                               if (!final_loggedin) {
+                                       android.widget.Toast toast = android.widget.Toast.makeText(
+                                               final_context, "You must login first to post reviews",
+                                               android.widget.Toast.LENGTH_SHORT);
+                                       toast.show();
+                                       return true;
+                               }
+
+                               // Build dialog with a simple interactive RatingBar
+                               final android.app.AlertDialog.Builder dialog_builder = new android.app.AlertDialog.Builder(final_context);
+                               final android.widget.RatingBar rating = new android.widget.RatingBar(final_context);
+                               rating.setNumStars(5);
+                               rating.setStepSize(1.0f);
+                               rating.setRating(final_rating);
+
+                               // Header bar
+                               int icon = final_context.getResources().getIdentifier("notif", "drawable", final_context.getPackageName());
+                               dialog_builder.setIcon(icon);
+                               dialog_builder.setTitle(final_title);
+
+                               // Rating control
+                               android.widget.LinearLayout l = new android.widget.LinearLayout(final_context);
+                               l.addView(rating, params);
+                               l.setHorizontalGravity(android.view.Gravity.CENTER_HORIZONTAL);
+                               dialog_builder.setView(l);
+
+                               // OK button
+                               dialog_builder.setPositiveButton(android.R.string.ok,
+                                       new android.content.DialogInterface.OnClickListener() {
+                                               public void onClick(android.content.DialogInterface dialog, int which) {
+                                                       dialog.dismiss();
+
+                                                       long r = (long)rating.getRating();
+                                                       view.setRating(r); // Update static control
+                                                       view.invalidate(); // For not refreshing bug
+
+                                                       BeerView_on_review(final_self, r); // Callback
+                                                       BeerView_decr_ref(final_self); // Nit GC
+                                               }
+                                       });
+
+                               // Cancel button
+                               dialog_builder.setNegativeButton(android.R.string.cancel,
+                                       new android.content.DialogInterface.OnClickListener() {
+                                               public void onClick(android.content.DialogInterface dialog, int id) {
+                                                       dialog.cancel();
+                                                       BeerView_decr_ref(final_self); // Nit GC
+                                               }
+                                       });
+
+                               dialog_builder.create();
+                               dialog_builder.show();
+                               return true;
+                       }
+               });
+       `}
+end
diff --git a/contrib/benitlux/src/client/android_proto.nit b/contrib/benitlux/src/client/android_proto.nit
new file mode 100644 (file)
index 0000000..22f0953
--- /dev/null
@@ -0,0 +1,28 @@
+# 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.
+
+# Android variant without modification, pure prototype
+#
+# Usually, compiling with `nitc -m android client.nit` is enough.
+# In this case, for research purposes we set a different `app_namespace`.
+# This allows both the proto and the adaptation to be installed on the same device.
+module android_proto is
+       app_name "Ben Proto"
+       app_namespace "net.xymus.benitlux_proto"
+       android_api_target 16
+end
+
+import ::android
+
+import client
diff --git a/contrib/benitlux/src/client/base.nit b/contrib/benitlux/src/client/base.nit
new file mode 100644 (file)
index 0000000..bcc1efd
--- /dev/null
@@ -0,0 +1,155 @@
+# 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.
+
+# Common services for the Benitlux app
+module base
+
+import app::ui
+import app::data_store
+import app::http_request
+import android::aware
+import json::serialization
+
+import benitlux_model
+import translations
+
+# Show debug output?
+fun debug: Bool do return true
+
+# Root URI of the remote RESTfule server
+fun benitlux_rest_server_uri: String do return "http://localhost:8080/"
+
+redef class App
+
+       # Current connection token, or "none"
+       var token: String is lazy, writable do
+               var token = app.data_store["token"]
+               if token isa String then return token
+               return "none"
+       end
+
+       # Name of the currently logged in user
+       var user: nullable String is lazy, writable do
+               var user = app.data_store["user"]
+               if user isa nullable String then return user
+               return null
+       end
+
+       # Event when user logs in or out
+       fun on_log_in do on_save_state
+
+       redef fun on_save_state
+       do
+               app.data_store["user"] = user
+               app.data_store["token"] = token
+               super
+       end
+
+       # Has this app state been restored yet?
+       var restored = false
+
+       redef fun on_restore_state
+       do
+               super
+
+               # TODO this may happen before the lazy loading above
+               restored = true
+
+               if token != "none" then on_log_in
+       end
+
+       # Show simple feedback to the user on important errors
+       fun feedback(text: Text) do print_error text
+end
+
+# Show a notification to the user
+fun notify(title, content: Text, uniqueness_id: Int)
+do print "Notification {uniqueness_id}: {title}; {content}"
+
+# View for an item in a list, like a beer or a person
+abstract class ItemView
+       super View
+end
+
+# Basic async HTTP request for this app
+#
+# Note that connection errors are passed to `on_fail`, and
+# server errors or authentification errors are received by `on_load`
+# and should be passed to `intercept_error`.
+class BenitluxHttpRequest
+       super AsyncHttpRequest
+
+       redef fun rest_server_uri do return benitlux_rest_server_uri
+
+       redef var rest_action
+
+       redef fun on_fail(error)
+       do
+               if error isa IOError then
+                       # This should be a normal network error like being offline.
+                       # Print to log, but don't show to the user.
+                       print_error error.class_name
+               else
+                       # This could be a deserialization error,
+                       # it may be related to an outdated client.
+                       # Report to user.
+                       print_error "Request Error: {rest_server_uri / rest_action} with {error}"
+                       app.feedback "Request Error: {error}"
+               end
+       end
+
+       # Intercept known server side errors
+       fun intercept_error(res: nullable Object): Bool
+       do
+               if res isa BenitluxTokenError then
+                       app.token = "none"
+                       app.user = null
+                       return true
+               else if res isa BenitluxError then
+                       app.feedback((res.user_message or else res.message).t)
+                       return true
+               else if res isa Error then
+                       app.feedback res.message.t
+                       return true
+               end
+               return false
+       end
+end
+
+# Async request with services to act on the windows of the app
+class WindowHttpRequest
+       super BenitluxHttpRequest
+
+       autoinit window, rest_action
+
+       # Type of the related `window`
+       type W: Window
+
+       # `Window` on which to apply the results of this request
+       var window: W
+
+       # `Views` to disable while this request is in progress
+       var affected_views = new Array[View]
+
+       redef fun before do for view in affected_views do view.enabled = false
+
+       redef fun after do for view in affected_views do view.enabled = true
+end
+
+redef class Text
+       # Ellipsize `self` so it fits within `max_length` characters
+       #
+       # FIXME Remove this when labels are correctly ellipsized on iOS.
+       fun ellipsize: Text do return self
+end
diff --git a/contrib/benitlux/src/client/client.nit b/contrib/benitlux/src/client/client.nit
new file mode 100644 (file)
index 0000000..e2fc2a9
--- /dev/null
@@ -0,0 +1,44 @@
+# 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.
+
+# Portable Benitlux app
+module client is
+       app_name "Benitlux"
+       app_version(0, 3, git_revision)
+       app_namespace "net.xymus.benitlux"
+end
+
+import home_views
+import beer_views
+import social_views
+import user_views
+
+# ---
+# Services
+
+redef class Deserializer
+       redef fun deserialize_class(name)
+       do
+               if name == "Array[Beer]" then return new Array[Beer].from_deserializer(self)
+               if name == "Array[User]" then return new Array[User].from_deserializer(self)
+               if name == "Array[BeerBadge]" then return new Array[BeerBadge].from_deserializer(self)
+               if name == "Array[BeerAndRatings]" then return new Array[BeerAndRatings].from_deserializer(self)
+               if name == "Array[String]" then return new Array[String].from_deserializer(self)
+               if name == "Array[UserAndFollowing]" then return new Array[UserAndFollowing].from_deserializer(self)
+               return super
+       end
+end
+
+set_fr
+super
diff --git a/contrib/benitlux/src/client/features/checkins.nit b/contrib/benitlux/src/client/features/checkins.nit
new file mode 100644 (file)
index 0000000..3be087e
--- /dev/null
@@ -0,0 +1,179 @@
+# 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.
+
+# On location checkin services
+module checkins
+
+import client
+
+redef class App
+
+       # Should we share our checkins with the server and friends?
+       fun share_checkins: Bool
+       do return app.data_store["share_checkins"].as(nullable Bool) or else true
+
+       # Should we share our checkins with the server and friends?
+       fun share_checkins=(value: Bool)
+       do
+               # Notify server
+               if currently_on_location then
+                       if value then
+                               server_check_in
+                       else server_check_out
+               end
+
+               app.data_store["share_checkins"] = value
+       end
+
+       # Are we currently at the location?
+       fun currently_on_location: Bool
+       do return app.data_store["currently_on_location"].as(nullable Bool) or else false
+
+       # Are we currently at the location?
+       fun currently_on_location=(value: Bool) do app.data_store["currently_on_location"] = value
+
+       # Request beer menu from the server
+       #
+       # It includes a diff if `checkins` remembers a previous visit.
+       fun request_menu
+       do
+               var checkins = checkins
+               var since = checkins.latest
+               if since != null then
+                       var today = today
+                       if since == today then
+                               since = checkins.previous
+                       end
+               end
+
+               (new MenuHttpRequest("rest/since?token={token}&date={since or else ""}")).start
+       end
+
+       # User checks in
+       fun on_check_in
+       do
+               if currently_on_location then return
+
+               if share_checkins then server_check_in
+
+               currently_on_location = true
+               request_menu
+               checkins.update today
+       end
+
+       # User checks out
+       fun on_check_out
+       do
+               if not currently_on_location then return
+
+               if share_checkins then server_check_out
+               currently_on_location = false
+       end
+
+       # Notify server of checkin
+       private fun server_check_in do (new BenitluxHttpRequest("rest/checkin?token={app.token}&is_in=true")).start
+
+       # Notify server of checkout
+       private fun server_check_out do (new BenitluxHttpRequest("rest/checkin?token={app.token}&is_in=false")).start
+
+       # History of the last 1 or 2 checkins
+       var checkins = new SimpleMemory
+
+       redef fun on_save_state
+       do
+               super
+               app.data_store["checkins"] = checkins
+       end
+
+       redef fun on_restore_state
+       do
+               var checkins = app.data_store["checkins"]
+               if checkins isa SimpleMemory then self.checkins = checkins
+
+               super
+       end
+end
+
+# Request the menu from the server for a notification
+class MenuHttpRequest
+       super BenitluxHttpRequest
+
+       redef fun on_load(data)
+       do
+               if not data isa Array[BeerAndRatings] then
+                       on_fail new Error("Server sent unexpected data {data or else "null"}")
+                       return
+               end
+
+               var content = data.beers_to_notification
+
+               notify("Passing by the Benelux?".t, content, 2)
+       end
+end
+
+# ---
+# Support services
+
+# Memory of an element and the previous one, avoiding duplication
+#
+# Used to remember the last day at the location,
+# ignoring multiple reports on the same day.
+class SimpleMemory
+       serialize
+
+       # Before latest remembered entry
+       var previous: nullable String = null
+
+       # Last remembered entry
+       var latest: nullable String = null
+
+       # Update `latest` if `value` is different
+       fun update(value: String)
+       do
+               if value == latest then return
+
+               previous = latest
+               latest = value
+       end
+end
+
+# ---
+# UI
+
+redef class UserWindow
+
+       private var lbl_checkins_options_title = new Label(parent=layout,
+               text="Share options".t)
+
+       private var chk_share_checkins = new CheckBox(parent=layout,
+               text="Share checkins with your friends".t)
+
+       init
+       do
+               chk_share_checkins.is_checked = app.share_checkins
+               lbl_checkins_options_title.size = 1.5
+       end
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ToggleEvent then
+                       var sender = event.sender
+                       if sender == chk_share_checkins then
+                               app.share_checkins = sender.is_checked
+                       end
+               end
+       end
+end
diff --git a/contrib/benitlux/src/client/features/debug.nit b/contrib/benitlux/src/client/features/debug.nit
new file mode 100644 (file)
index 0000000..ae7be3c
--- /dev/null
@@ -0,0 +1,67 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Debugging features accessible from the user preference menu
+module debug
+
+import client
+import push
+import checkins
+
+redef class UserWindow
+
+       private var layout_debug = new VerticalLayout(parent=layout)
+
+       private var lbl_debug_title = new Label(parent=layout_debug,
+               text="Debug options".t)
+
+       private var but_test_notif = new Button(parent=layout_debug,
+               text="Test notifications".t)
+
+       private var but_test_checkin = new Button(parent=layout_debug,
+               text="Test checkin".t)
+
+       private var but_test_checkout = new Button(parent=layout_debug,
+               text="Test checkout".t)
+
+       private var but_test_menu = new Button(parent=layout_debug,
+               text="Test menu diff".t)
+
+       init
+       do
+               lbl_debug_title.size = 1.5
+
+               for c in [but_test_notif, but_test_checkin, but_test_checkout, but_test_menu] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_test_notif then
+                               notify("Test Notification", "Some content\nmultiline", 5)
+                       else if sender == but_test_checkin then
+                               app.on_check_in
+                       else if sender == but_test_checkout then
+                               app.on_check_out
+                       else if sender == but_test_menu then
+                               app.request_menu
+                       end
+               end
+       end
+end
diff --git a/contrib/benitlux/src/client/features/push.nit b/contrib/benitlux/src/client/features/push.nit
new file mode 100644 (file)
index 0000000..3006616
--- /dev/null
@@ -0,0 +1,222 @@
+# 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.
+
+# Push notification support
+module push
+
+import app::http_request
+
+import client
+
+redef class App
+       redef fun on_log_in
+       do
+               super
+               #(new PushHttpRequest("push/check_token?token={app.token}")).start
+       end
+
+       # Names of the known users currently on location
+       var users_on_location = new Set[String]
+
+       # Should we show a daily notification when new beers are available?
+       fun notify_on_new_beers: Bool
+       do return app.data_store["notify_on_new_beers"].as(nullable Bool) or else true
+
+       # Should we show a daily notification when new beers are available?
+       fun notify_on_new_beers=(value: Bool) do app.data_store["notify_on_new_beers"] = value
+
+       # Should we show a daily notification of the menu?
+       fun notify_menu_daily: Bool
+       do return app.data_store["notify_menu_daily"].as(nullable Bool) or else false
+
+       # Should we show a daily notification of the menu?
+       fun notify_menu_daily=(value: Bool) do app.data_store["notify_menu_daily"] = value
+
+       # Should we show a notification when friends check in at the location?
+       fun notify_on_checkins: Bool
+       do return app.data_store["notify_on_checkins"].as(nullable Bool) or else true
+
+       # Should we show a notification when friends check in at the location?
+       fun notify_on_checkins=(value: Bool) do app.data_store["notify_on_checkins"] = value
+end
+
+# Open push notification request
+class PushHttpRequest
+       super BenitluxHttpRequest
+
+       redef fun on_fail(error)
+       do
+               if app.user == null then return
+
+               super
+
+               print_error "{class_name}: on_fail {error}"
+
+               var t = new PushHttpRequest("push/?token={app.token}")
+               t.delay = 10.0
+               t.start
+       end
+
+       redef fun on_load(data)
+       do
+               if app.user == null then return
+
+               var delay = 0.0
+               if data isa Pushable then
+                       data.apply_push_if_desired
+               else if data isa BenitluxError then
+                       # TODO if forbidden ask for a new token
+                       delay = 5.0*60.0
+               else
+                       print_error "{class_name}: Received {data or else "null"}"
+               end
+
+               var t = new PushHttpRequest("push/?token={app.token}")
+               t.delay = delay
+               t.start
+       end
+end
+
+# ---
+# Objects sent from the server to the client
+
+# Objects sent as push notifications by the server
+interface Pushable
+       # Act on this push notification
+       fun apply_push do print_error "Unimplemented `apply_push` on {class_name}"
+
+       # Consider to `apply_push` if the user preferences wants to
+       fun apply_push_if_desired do apply_push
+end
+
+redef class CheckinReport
+       super Pushable
+
+       # Flattened array of the name of users
+       var user_names: Array[String] = [for u in users do u.name] is lazy
+
+       redef fun apply_push_if_desired
+       do
+               if not app.notify_on_checkins then return
+
+               var there_is_a_new_user = false
+               for new_users in user_names do
+                       if not app.users_on_location.has(new_users) then
+                               there_is_a_new_user = true
+                               break
+                       end
+               end
+
+               app.users_on_location.clear
+               app.users_on_location.add_all user_names
+
+               # Apply only if there is someone new on location
+               if there_is_a_new_user then super
+       end
+
+       redef fun apply_push
+       do
+               if users.is_empty then
+                       #app.notif_push.cancel
+                       #self.cancel(tag, (int)id);
+                       return
+               end
+
+               var title = "TTB!".t
+               var names = [for user in users do user.name]
+               var content = "From %0".t.format(names.join(", "))
+
+               notify(title, content, 1)
+       end
+end
+
+redef class DailyNotification
+       super Pushable
+
+       redef fun apply_push_if_desired
+       do
+               if app.notify_menu_daily then
+                       super
+                       return
+               end
+
+               if app.notify_on_new_beers then
+                       for beer in beers do
+                               if beer.is_new then
+                                       super
+                                       return
+                               end
+                       end
+               end
+       end
+
+       redef fun apply_push
+       do
+               var title = if beers.has_new_beers then
+                       "New beers are on the menu".t
+               else "Beer Menu".t
+
+               var content = beers.beers_to_notification
+               notify(title, content, 3)
+       end
+end
+
+# ---
+# UI
+
+redef class UserWindow
+
+       private var layout_push_options = new VerticalLayout(parent=layout)
+
+       private var lbl_push_options_title = new Label(parent=layout_push_options,
+               text="Notifications options".t)
+
+       private var chk_notify_on_new_beers = new CheckBox(parent=layout_push_options,
+               text="Notify when there are new beers".t)
+
+       private var chk_notify_menu_daily = new CheckBox(parent=layout_push_options,
+               #text="Show the menu every work day?".t)
+               text="Show the menu every work day".t)
+
+       private var chk_notify_on_checkins = new CheckBox(parent=layout_push_options,
+               text="Notify when a friend checks in".t)
+
+       init
+       do
+               lbl_push_options_title.size = 1.5
+               chk_notify_on_new_beers.is_checked = app.notify_on_new_beers
+               chk_notify_menu_daily.is_checked = app.notify_menu_daily
+               chk_notify_on_checkins.is_checked = app.notify_on_checkins
+
+               for c in [chk_notify_menu_daily, chk_notify_on_new_beers, chk_notify_on_checkins] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ToggleEvent then
+                       var sender = event.sender
+                       if sender == chk_notify_on_new_beers then
+                               app.notify_on_new_beers = sender.is_checked
+                       else if sender == chk_notify_menu_daily then
+                               app.notify_menu_daily = sender.is_checked
+                       else if sender == chk_notify_on_checkins then
+                               app.notify_on_checkins = sender.is_checked
+                       end
+               end
+       end
+end
diff --git a/contrib/benitlux/src/client/features/translations.nit b/contrib/benitlux/src/client/features/translations.nit
new file mode 100644 (file)
index 0000000..02deb61
--- /dev/null
@@ -0,0 +1,103 @@
+# 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
+
+# Support for translating the app to different languages, implements French
+module translations
+
+redef class Text
+       # Translate `self` according to the current language `sys.lang`
+       fun t: String
+       do
+               var lang = sys.lang_map
+               if lang == null then return to_s
+
+               if lang.keys.has(self) then return lang[self]
+
+               print "Translation miss ({sys.lang}): {self}"
+               return to_s
+       end
+end
+
+redef class Sys
+       # Name of the language in use
+       var lang = "C"
+
+       # Translation map for the language in use
+       var lang_map: nullable Map[Text, String] = null
+end
+
+# Set French as the current language
+fun set_fr
+do
+       var map = new Map[Text, String]
+
+       # Home views
+       map["Welcome %0"] = "Bienvenue %0"
+       map["Welcome"] = "Bienvenue"
+       map["Beer Menu"] = "Menu de bières"
+       map["View all"] = "Menu complet"
+       map["Preferences"] = "Préférences"
+       map["Friends"] = "Amis"
+       map["Manage"] = "Gérer"
+       map["Events"] = "Événements"
+       map["Loading..."] = "Chargement..."
+       map["Login or signup"] = "S'authentifier"
+       map["On location?"] = "Sur place?"
+       map["Leaving?"] = "Vous quittez?"
+
+       # User/login views
+       map["Account options"] = "Options du compte"
+       map["Share options"] = "Options de partage"
+       map["Notifications options"] = "Options de notification"
+       map["Please login"] = "Veuillez vous identifier"
+       map["Welcome %0!"] = "Bienvenue %0!"
+       map["Logged in as %0"] = "Connecté en tant que %0"
+       map["Username"] = "Nom d'utilisateur"
+       map["Invalid name"] = "Nom d'utilisateur invalide"
+       map["Password"] = "Mot de passe"
+       map["Passwords must be composed of at least 6 characters."] = "Le mot de passe doit avoir au moins 6 charactères."
+       map["Email"] = "Courriel"
+       map["Login"] = "Se connecter"
+       map["Logout"] = "Se déconnecter"
+       map["Signup"] = "Créer un compte"
+
+       # Social views
+       map["Follow"] = "Suivre"
+       map["Unfollow"] = "Ne plus suivre"
+       map["Search"] = "Rechercher"
+       map["Favorites: %0"] = "Favoris: %0"
+       map["No favorites yet"] = "Pas de favoris"
+       map["List followed"] = "Personnes suivies"
+       map["List followers"] = "Personnes vous suivant"
+
+       # Beer views
+       map["Review %0"] = "Évaluer %0"
+       map["%0★ %1 reviews"] = "%0★ %1 avis"
+       map["No reviews yet"] = "Aucun avis"
+       map[", friends: %0☆ %1 reviews"] = ", amis: %0☆ %1 avis"
+       map[" (New)"] = " (Nouveau)"
+       map["Similar to %0."] = "Similaire à %0."
+       map["Favorite beer on the menu."] = "Bière préférée sur le menu."
+       map["Favorite of %0"] = "Préférée de %0"
+
+       # Preferences
+       map["Notify when a friend checks in"] = "Lorsqu'un ami est sur place"
+       map["Show the menu every work day"] = "Menu journalier en semaine"
+       map["Notify when there are new beers"] = "Lorsqu'il y a de nouvelles bières"
+       map["Share checkins with your friends"] = "Partager lorsque vous êtes sur place"
+       map["Passing by the Benelux?"] = "De passage au Bénélux?"
+
+       # Update Sys
+       sys.lang = "fr"
+       sys.lang_map = map
+end
diff --git a/contrib/benitlux/src/client/ios.nit b/contrib/benitlux/src/client/ios.nit
new file mode 100644 (file)
index 0000000..9601202
--- /dev/null
@@ -0,0 +1,147 @@
+# 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.
+
+# iOS variant using a button to check in/out and local notifications
+module ios
+
+import ::ios
+intrude import app::ui
+
+import client
+import push
+import checkins
+
+redef class HomeWindow
+       init
+       do
+               title = "Benitlux"
+               update_checkin_text
+               checkin_button.observers.add self
+       end
+
+       # TODO hide when not logged in
+       private var layout_login_checkin = new HorizontalLayout(parent=layout_user)
+       private var checkin_label = new Label(parent=layout_login_checkin)
+       private var checkin_button = new Button(parent=layout_login_checkin)
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == checkin_button then
+                               if app.currently_on_location then
+                                       app.on_check_out
+                               else app.on_check_in
+                       end
+               end
+       end
+
+       private fun update_checkin_text
+       do
+               if app.currently_on_location then
+                       checkin_label.text = "Leaving?".t
+                       checkin_button.text = "Check out".t
+               else
+                       checkin_label.text = "On location?".t
+                       checkin_button.text = "Check in".t
+               end
+       end
+end
+
+redef class App
+       redef fun on_check_in
+       do
+               super
+               var window = window
+               if window isa HomeWindow then window.update_checkin_text
+       end
+
+       redef fun on_check_out
+       do
+               super
+               var window = window
+               if window isa HomeWindow then window.update_checkin_text
+       end
+
+       redef fun did_finish_launching_with_options
+       do
+               ui_application.register_user_notification_settings
+               return super
+       end
+end
+
+redef class UserWindow
+       init do title = "Preferences".t
+end
+
+redef class BeersWindow
+       init do title = "Beers".t
+end
+
+redef class SocialWindow
+       init do title = "People".t
+end
+
+# --- Notifications
+
+redef fun notify(title, content, id)
+do native_notify(title.to_nsstring, content.to_nsstring)
+
+private fun native_notify(title, content: NSString) in "ObjC" `{
+       UILocalNotification* notif = [[UILocalNotification alloc] init];
+       notif.alertTitle = title;
+       notif.alertBody = content;
+       notif.timeZone = [NSTimeZone defaultTimeZone];
+       [[UIApplication sharedApplication] presentLocalNotificationNow: notif];
+`}
+
+redef class UIApplication
+
+       # Register this app to display notifications
+       private fun register_user_notification_settings
+       in "ObjC" `{
+               if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]){
+                       [self registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
+               }
+       `}
+end
+
+# ---
+# Shorten labels
+
+redef class Label
+       # Ellipsize `text` so it fits within `max_length` characters
+       #
+       # FIXME Remove this when labels are correctly ellipsized on iOS.
+       redef fun text=(text)
+       do
+               if text == null then
+                       super
+                       return
+               end
+
+               var max_length = 50
+               if parent isa HorizontalLayout and parent.parent isa BeerView then
+                       # This is the name of a beer, remember its a hack
+                       max_length = 20
+               end
+
+               if text.length > max_length then
+                       text = text.substring(0, max_length - 3).to_s + "..."
+               end
+               super text
+       end
+end
diff --git a/contrib/benitlux/src/client/views/beer_views.nit b/contrib/benitlux/src/client/views/beer_views.nit
new file mode 100644 (file)
index 0000000..54ad9e9
--- /dev/null
@@ -0,0 +1,329 @@
+# 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.
+
+# Window to list beers and other beer-related views
+module beer_views
+
+import base
+
+# View about a beer, with its name, description and rating
+class BeerView
+       super VerticalLayout
+       super ItemView
+
+       autoinit beer_info, parent
+
+       # Beer information
+       var beer_info: BeerAndRatings
+
+       # Buttons to realize the rating buttons
+       var star_buttons = new Array[StarButton]
+
+       # Layout of the first line with the name and `star_buttons`
+       var top_line_layout = new HorizontalLayout(parent=self)
+
+       init
+       do
+               var lbl_name = new Label(parent=top_line_layout, text=beer_info.beer.name)
+               lbl_name.size = 1.25
+
+               var desc = beer_info.beer.desc
+               if beer_info.is_new then desc += " (New)".t
+               var lbl_desc = new Label(parent=self, text=desc)
+
+               var lbl_stats = new Label(parent=self, text=beer_info.rating_text)
+               lbl_stats.size = 0.5
+
+               var badges = beer_info.badges
+               if badges != null then
+                       var lbl_comment = new Label(parent=self, text=badges.join(" "))
+                       lbl_comment.size = 0.5
+               end
+
+               var rating = beer_info.user_rating or else 0
+               setup_stars rating
+       end
+
+       # Prepare and display the rating controls
+       fun setup_stars(rating: Int)
+       do
+               var l_stars = new HorizontalLayout(parent=top_line_layout)
+
+               for i in [1..5] do
+                       var but = new StarButton(beer_info.beer, i, i <= rating, parent=l_stars)
+                       but.size = 1.5
+                       but.observers.add self
+                       star_buttons.add but
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               assert event isa ButtonPressEvent
+
+               var sender = event.sender
+               if sender isa StarButton then
+                       on_review sender.rating
+               end
+       end
+
+       # Post a user review
+       fun on_review(rating: Int)
+       do
+               var beer_id = beer_info.beer.id
+               (new ReviewAction(app.window, "rest/review?token={app.token}&beer={beer_id}&rating={rating}")).start
+
+               # Update UI
+               var i = 1
+               for but in star_buttons do
+                       but.on = i <= rating
+                       i += 1
+               end
+       end
+end
+
+# Beers pane listing the available beers
+class BeersWindow
+       super Window
+
+       private var layout = new VerticalLayout(parent=self)
+       private var list_beers = new ListLayout(parent=layout)
+
+       init
+       do
+               if debug then print "BenitluxWindow::init"
+
+               action_list_beers
+       end
+
+       # Send HTTP request to list beers
+       fun action_list_beers
+       do (new ListBeersAction(self, "rest/list?token={app.token}")).start
+end
+
+# ---
+# Customized buttons
+
+# View to describe and rate a eer
+class RatingView
+       super View
+
+       autoinit beer, init_rating, parent, enabled
+
+       # Beer id
+       var beer: Beer
+
+       # Previous rating, 0 for none
+       var init_rating: Int
+
+       redef fun parent=(layout) do end
+
+       redef fun enabled=(value) do end
+end
+
+# Button with a star, filled or not, for rating beers
+class StarButton
+       super Button
+
+       autoinit beer, rating, on, parent, enabled
+
+       # Info on the beer to rate
+       var beer: Beer
+
+       # Rating of `beer`
+       var rating: Int
+
+       # Set if the star is filled
+       fun on=(on: Bool) is autoinit do text = if on then "★" else "☆"
+end
+
+redef class BeerAndRatings
+       # Text version of the ratings
+       fun rating_text: String
+       do
+               var txt = new Array[String]
+
+               var global = global
+               if global != null and global.count > 0 then
+                       txt.add "%0★ %1 reviews".t.format(global.average.to_precision(1), global.count)
+               else txt.add "No reviews yet".t
+
+               var local = followed
+               if local != null and local.count > 0 then
+                       txt.add ", friends: %0☆ %1 reviews".t.format(local.average.to_precision(1), local.count)
+               end
+
+               return txt.join
+       end
+end
+
+redef class Beer
+       # Capitalize first letter for a prettier display
+       redef fun desc
+       do
+               var desc = super
+               if desc.length == 0 then return desc
+
+               var first_letter = desc.first.to_upper
+               return first_letter.to_s + desc.substring_from(1)
+       end
+end
+
+# Comparator of beers
+class BeerComparator
+       super Comparator
+
+       redef type COMPARED: BeerAndRatings
+
+       redef fun compare(a, b) do return value_of(a) <=> value_of(b)
+
+       private fun value_of(beer: COMPARED): Float
+       do
+               var max = 0.0
+               var value = 0.0
+
+               var rating = beer.user_rating
+               if rating != null then
+                       max += 20.0
+                       value += rating.to_f * 4.0
+               end
+
+               var followed = beer.followed
+               if followed != null then
+                       max += 10.0
+                       value += followed.average * 2.0
+               end
+
+               var global = beer.global
+               if global != null then
+                       max += 5.0
+                       value += global.average
+               end
+
+               return (max - value)/max
+       end
+end
+
+# Async request to submit a review
+class ReviewAction
+       super WindowHttpRequest
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+       end
+end
+
+# Async request to update the beer list
+class ListBeersAction
+       super WindowHttpRequest
+
+       redef type W: BeersWindow
+
+       redef fun on_load(beers)
+       do
+               window.layout.remove window.list_beers
+               window.list_beers = new ListLayout(parent=window.layout)
+
+               if intercept_error(beers) then return
+
+               if not beers isa Array[BeerAndRatings] then
+                       app.feedback "Communication Error".t
+                       return
+               end
+
+               # Sort beers per preference
+               var comparator = new BeerComparator
+               comparator.sort beers
+
+               # Populate the list
+               for beer_and_rating in beers do
+                       var view = new BeerView(beer_and_rating, parent=window.list_beers)
+               end
+       end
+end
+
+redef class BestBeerBadge
+       redef fun to_s do return "Favorite beer on the menu.".t
+end
+
+redef class FavoriteBeerBadge
+       redef fun to_s do return "Favorite of %0.".t.format(users.join(", ", " & "))
+end
+
+redef class SimilarBeerBadge
+       redef fun to_s do return "Similar to %0.".t.format(beers.join(", ", " & "))
+end
+
+redef class Array[E]
+       # Pretty compressed list of this list of beer as a pseudo diff
+       #
+       # Require: `self isa Array[BeerAndRatings]`
+       fun beers_to_notification: String
+       do
+               assert self isa Array[BeerAndRatings]
+
+               # Sort beers per preference
+               var comparator = new BeerComparator
+               comparator.sort self
+
+               # Organize the notification line per line
+               # First the new beers, then the fixed one.
+               var lines = new Array[String]
+               var fix_beers = new Array[String]
+               for bar in self do
+                       var beer = bar.beer
+                       if bar.is_new then
+                               lines.add "+ {beer.name}: {beer.desc}"
+                       else fix_beers.add beer.name
+               end
+
+               # Show a few fixed beers per line
+               if fix_beers.not_empty then
+                       var line = new FlatBuffer
+                       line.append "= "
+                       for i in fix_beers.length.times, beer in fix_beers do
+
+                               if i > 0 then line.append ", "
+
+                               var l = line.length + beer.length
+                               if l < 42 then # Very approximate width of a notification on Android
+                                       line.append beer
+                                       continue
+                               end
+
+                               lines.add line.to_s
+
+                               line = new FlatBuffer
+                               line.append "= "
+                               line.append beer
+                       end
+
+                       lines.add line.to_s
+               end
+
+               return lines.join("\n")
+       end
+
+       # Does `self` has a new beer?
+       #
+       # Require: `self isa Array[BeerAndRatings]`
+       fun has_new_beers: Bool
+       do
+               assert self isa Array[BeerAndRatings]
+
+               for beer in self do if beer.is_new then return true
+               return false
+       end
+end
diff --git a/contrib/benitlux/src/client/views/home_views.nit b/contrib/benitlux/src/client/views/home_views.nit
new file mode 100644 (file)
index 0000000..c991f3c
--- /dev/null
@@ -0,0 +1,211 @@
+# 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.
+
+# Main home window
+module home_views
+
+import beer_views
+import social_views
+import user_views
+
+redef class App
+       redef fun on_create
+       do
+               if debug then print "App::on_create"
+
+               # Create the main window
+               show_home
+               super
+       end
+
+       # Show the home/main windows
+       fun show_home
+       do
+               var window = new HomeWindow
+               window.refresh
+               push_window window
+       end
+
+       redef fun on_log_in
+       do
+               super
+
+               # Send back to the home window when logging in
+               if not window isa HomeWindow then pop_window
+       end
+end
+
+# Social pane with networking features
+class HomeWindow
+       super Window
+
+       private var layout = new ListLayout(parent=self)
+
+       # Cut-point for the iOS adaptation
+       var layout_user = new VerticalLayout(parent=layout)
+       private var layout_login = new HorizontalLayout(parent=layout_user)
+       private var lbl_login_status = new Label(parent=layout_login, text="Welcome".t, size=1.5)
+       private var but_login = new Button(parent=layout_login, text="Login or signup".t)
+       private var but_preferences = new Button(parent=layout_login, text="Preferences".t)
+
+       private var layout_beers = new VerticalLayout(parent=layout)
+       var layout_beers_title = new HorizontalLayout(parent=layout_beers)
+       var title_beers = new SectionTitle(parent=layout_beers_title, text="Beer Menu".t, size=1.5)
+       private var beer_button = new Button(parent=layout_beers_title, text="View all".t)
+       private var beer_list = new VerticalLayout(parent=layout_beers)
+       private var beer_temp_lbl = new Label(parent=beer_list, text="Loading...".t)
+
+       private var layout_social = new VerticalLayout(parent=layout)
+       private var social_header = new HorizontalLayout(parent=layout_social)
+       private var social_title = new SectionTitle(parent=social_header, text="Friends".t, size=1.5)
+       private var social_button = new Button(parent=social_header, text="Manage".t)
+       private var social_list = new VerticalLayout(parent=layout_social)
+       private var social_temp_lbl = new Label(parent=social_list, text="Loading...".t)
+
+       private var layout_news = new VerticalLayout(parent=layout)
+       private var news_header = new HorizontalLayout(parent=layout_news)
+       private var news_title = new SectionTitle(parent=news_header, text="Events".t, size=1.5)
+       #private var news_button = new Button(parent=news_header, text="Open website") # TODO
+       private var news_label = new Label(parent=layout_news, text="Bière en cask le jeudi!")
+
+       init
+       do
+               for c in [but_login, but_preferences, beer_button, social_button] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_resume do refresh
+
+       # Refresh content of this page
+       fun refresh
+       do
+               if not app.restored then return
+
+               layout_login.clear
+               if app.user != null then
+                       # Logged in
+                       lbl_login_status.parent = layout_login
+                       but_preferences.parent = layout_login
+                       lbl_login_status.set_welcome
+               else
+                       but_login.parent = layout_login
+                       but_preferences.parent = layout_login
+               end
+
+               # Fill beers
+               (new ListDiffAction(self, "rest/since?token={app.token}")).start
+
+               # Fill people
+               (new HomeListPeopleAction(self, "rest/friends?token={app.token}")).start
+
+               # Check if token is still valid
+               (new CheckTokenAction(self, "rest/check_token?token={app.token}")).start
+       end
+
+       redef fun on_event(event)
+       do
+               if debug then print "BenitluxWindow::on_event {event}"
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_preferences then
+                               app.push_window new UserWindow
+                               return
+                       else if sender == but_login then
+                               app.push_window new SignupWindow
+                               return
+                       else if sender == beer_button then
+                               app.push_window new BeersWindow
+                               return
+                       else if sender == social_button then
+                               app.push_window new SocialWindow
+                               return
+                       #else if sender == news_button then
+                               # TODO open browser?
+                       end
+               end
+
+               super
+       end
+end
+
+# `Label` used in section headers
+class SectionTitle super Label end
+
+# Async request to update the beer list on the home screen
+class ListDiffAction
+       super WindowHttpRequest
+
+       redef type W: HomeWindow
+
+       redef fun on_load(beers)
+       do
+               window.layout_beers.remove window.beer_list
+               window.beer_list = new VerticalLayout(parent=window.layout_beers)
+
+               if intercept_error(beers) then return
+
+               if not beers isa Array[BeerAndRatings] then
+                       app.feedback "Communication Error".t
+                       return
+               end
+
+               # Sort beers per preference
+               var comparator = new BeerComparator
+               comparator.sort beers
+
+               var max_beers = 6
+               while beers.length > max_beers do beers.pop
+
+               for bar in beers do
+                       var view = new BeerView(bar, parent=window.beer_list)
+               end
+       end
+end
+
+# Async request to list users
+class HomeListPeopleAction
+       super WindowHttpRequest
+
+       redef type W: HomeWindow
+
+       redef fun on_load(users)
+       do
+               window.layout_social.remove window.social_list
+               window.social_list = new VerticalLayout(parent=window.layout_social)
+
+               if intercept_error(users) then return
+
+               if users isa Array[UserAndFollowing] then for uaf in users do
+                       var view = new PeopleView(uaf, true, parent=window.social_list)
+               end
+       end
+end
+
+# Async request to check if `app.token` is still valid
+class CheckTokenAction
+       super WindowHttpRequest
+
+       redef type W: HomeWindow
+
+       redef fun on_load(res) do intercept_error(res)
+end
+
+# Today's date as a `String`
+fun today: String
+do
+       var tm = new Tm.localtime
+       return "{tm.year+1900}-{tm.mon+1}-{tm.mday}"
+end
diff --git a/contrib/benitlux/src/client/views/social_views.nit b/contrib/benitlux/src/client/views/social_views.nit
new file mode 100644 (file)
index 0000000..35cb6bd
--- /dev/null
@@ -0,0 +1,201 @@
+# 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.
+
+# Window to list beers and other beer-related views
+module social_views
+
+import base
+
+# Social pane with networking features
+class SocialWindow
+       super Window
+
+       private var layout = new VerticalLayout(parent=self)
+
+       private var list_search = new ListLayout(parent=layout)
+
+       private var layout_header = new VerticalLayout(parent=list_search)
+       private var layout_search = new HorizontalLayout(parent=layout_header)
+       private var txt_query = new TextInput(parent=layout_search)
+       private var but_search = new Button(parent=layout_search, text="Search".t)
+
+       private var layout_list = new HorizontalLayout(parent=layout_header)
+       private var but_followed = new Button(parent=layout_list, text="List followed".t)
+       private var but_followers = new Button(parent=layout_list, text="List followers".t)
+
+       init
+       do
+               for c in [but_search, but_followed, but_followers] do
+                       c.observers.add self
+               end
+
+               # Load friends and suggestions
+               (new ListUsersAction(self, "rest/friends?token={app.token}&n=16")).start
+       end
+
+       redef fun on_event(event)
+       do
+               if debug then print "BenitluxWindow::on_event {event}"
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_search then
+                               search
+                       else if sender == but_followed then
+                               var cmd = "rest/followed?token={app.token}"
+                               (new ListUsersAction(self, cmd)).start
+                       else if sender == but_followers then
+                               var cmd = "rest/followers?token={app.token}"
+                               (new ListUsersAction(self, cmd)).start
+                       end
+               end
+
+               super
+       end
+
+       # Execute search with `txt_query.text`
+       fun search
+       do
+               var query = txt_query.text
+               if query == null or query.is_empty then return
+
+               var res = "rest/search?token={app.token}&query={query}&offset=0"
+               (new ListUsersAction(self, res)).start
+       end
+
+       # Fill `list_search` with views for each of `users`
+       fun list_users(users: Array[UserAndFollowing])
+       do
+               for uaf in users do
+                       var view = new PeopleView(uaf, false, parent=list_search)
+               end
+       end
+end
+
+# View to describe, and follow a person
+class PeopleView
+       super VerticalLayout
+       super ItemView
+
+       autoinit user_and_following, home_window_mode, parent
+
+       # Description of the user
+       var user_and_following: UserAndFollowing
+
+       # Toggle tweaks for the home window where the is no "unfollow" buttons
+       var home_window_mode: Bool
+
+       init
+       do
+               var user = user_and_following.user
+
+               var layout_top_line = new HorizontalLayout(parent=self)
+               var lbl_name = new Label(parent=layout_top_line, text=user.name)
+
+               if app.user != null then
+
+                       # Show unfollow button if not on the home screen
+                       if not home_window_mode or not user_and_following.following then
+                               var but = new FollowButton(user.id, user_and_following.following, user_and_following.followed, parent=layout_top_line)
+                               but.observers.add self
+                       end
+               end
+
+               var favs = if not user_and_following.favs.is_empty then
+                       "Favorites: %0".t.format(user_and_following.favs)
+               else "No favorites yet".t
+               var lbl_desc = new Label(parent=self, text=favs, size=0.5)
+       end
+end
+
+# Button to follow or unfollow a user
+class FollowButton
+       super Button
+
+       autoinit followed_id, following, followed_by, parent, enabled, text
+
+       # Id of the user to be followd/unfollow
+       var followed_id: Int
+
+       # Does the local user already follows `followed_id`
+       var following: Bool
+
+       # Does `followed_id` already follows the local user
+       var followed_by: Bool
+
+       # Update the visible text according to `following`
+       fun update_text do text = if following then "Unfollow".t else "Follow".t
+
+       init do update_text
+
+       redef fun on_event(event)
+       do
+               assert event isa ButtonPressEvent
+               var cmd = "rest/follow?token={app.token}&user_to={followed_id}&follow={not following}"
+               enabled = false
+               text = "Updating...".t
+               (new FollowAction(app.window, cmd, self)).start
+       end
+end
+
+# Async request to receive and display a list of users
+#
+# This is used by many features of the social window:
+# search, list followed and list followers.
+class ListUsersAction
+       super WindowHttpRequest
+
+       redef type W: SocialWindow
+
+       init do affected_views.add_all([window.but_search, window.but_followed, window.but_followers])
+
+       redef fun on_load(users)
+       do
+               window.layout.remove window.list_search
+               window.list_search = new ListLayout(parent=window.layout)
+               window.layout_header.parent = window.list_search
+
+               if intercept_error(users) then return
+
+               if users isa Array[UserAndFollowing] then window.list_users users
+       end
+end
+
+# Async request to follow or unfollow a user
+class FollowAction
+       super WindowHttpRequest
+
+       private var button: FollowButton
+       init do affected_views.add(button)
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+       end
+
+       redef fun after
+       do
+               button.following = not button.following
+               button.update_text
+               button.enabled = true
+
+               super
+       end
+
+       redef fun before
+       do
+               button.enabled = false
+               super
+       end
+end
diff --git a/contrib/benitlux/src/client/views/user_views.nit b/contrib/benitlux/src/client/views/user_views.nit
new file mode 100644 (file)
index 0000000..e45fcb5
--- /dev/null
@@ -0,0 +1,215 @@
+# 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.
+
+# User preference window and other user-related view
+module user_views
+
+import base
+
+redef class Label
+       # Update the content of `lbl_welcome`
+       fun set_user_name
+       do
+               var name = app.user
+               self.text = if name != null then
+                       "Logged in as %0".t.format(name)
+               else "Not logged in".t
+       end
+
+       # Set `text` to welcome an authentified user or invite to authentify
+       fun set_welcome
+       do
+               var name = app.user
+               self.text = if name != null then
+                       "Welcome %0".t.format(name)
+               else ""
+       end
+end
+
+# User preference window
+class UserWindow
+       super Window
+
+       # Main window layout
+       var layout = new ListLayout(parent=self)
+
+       private var layout_user_options = new VerticalLayout(parent=layout)
+
+       private var lbl_user_options_title = new Label(parent=layout_user_options,
+               text="Account options".t)
+
+       private var lbl_welcome = new Label(parent=layout_user_options)
+       private var but_logout = new Button(parent=layout_user_options, text="Logout".t)
+
+       # Refesh displayed text
+       fun refresh
+       do
+               lbl_user_options_title.size = 1.5
+               lbl_welcome.set_user_name
+               but_logout.enabled = app.user != null
+       end
+
+       init
+       do
+               but_logout.observers.add self
+               refresh
+       end
+
+       redef fun on_event(event)
+       do
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_logout then
+                               app.user = null
+                               app.token = "none"
+                               app.on_log_in
+                               refresh
+                       end
+               end
+
+               super
+       end
+end
+
+# Window for signing up a new user or logging in
+class SignupWindow
+       super Window
+
+       # Main window layout
+       var layout = new ListLayout(parent=self)
+
+       private var lbl_welcome = new Label(parent=layout, text="Welcome")
+
+       # Name
+       private var name_line = new HorizontalLayout(parent=layout)
+       private var lbl_name = new Label(parent=name_line, text="Username".t)
+       private var txt_name = new TextInput(parent=name_line, text=app.user)
+
+       # Pass
+       private var pass_line = new HorizontalLayout(parent=layout)
+       private var lbl_pass = new Label(parent=pass_line, text="Password".t)
+       private var txt_pass = new TextInput(parent=pass_line, is_password=true)
+       private var lbl_pass_desc = new Label(parent=layout,
+               text="Passwords must be composed of at least 6 characters.".t)
+
+       private var but_login = new Button(parent=layout, text="Login".t)
+
+       # Email
+       private var email_line = new HorizontalLayout(parent=layout)
+       private var lbl_email = new Label(parent=email_line, text="Email".t)
+       private var txt_email = new TextInput(parent=email_line)
+
+       private var but_signup = new Button(parent=layout, text="Signup".t)
+
+       private var lbl_feedback = new Label(parent=layout, text="")
+
+       init
+       do
+               lbl_pass_desc.size = 0.5
+
+               for c in [but_login, but_signup] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               if debug then print "BenitluxWindow::on_event {event}"
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_login or sender == but_signup then
+
+                               var name = txt_name.text
+                               if name == null or not name.name_is_ok then
+                                       feedback "Invalid name".t
+                                       return
+                               end
+
+                               var pass = txt_pass.text
+                               if pass == null or not pass.pass_is_ok then
+                                       feedback "Invalid password".t
+                                       return
+                               end
+
+                               if sender == but_login then
+                                       (new LoginOrSignupAction(self, "rest/login?name={name}&pass={pass.pass_hash}")).start
+                               else if sender == but_signup then
+                                       var email = txt_email.text
+                                       if email == null or email.is_empty then
+                                               feedback "Invalid email".t
+                                               return
+                                       end
+
+                                       (new LoginOrSignupAction(self, "rest/signup?name={name}&pass={pass.pass_hash}&email={email}")).start
+                               end
+                       end
+               end
+
+               super
+       end
+
+       # Show lasting feedback to the user in a label
+       fun feedback(text: String) do lbl_feedback.text = text
+end
+
+# ---
+# Async RESTful actions
+
+# Async request for login in or signing up
+class LoginOrSignupAction
+       super WindowHttpRequest
+
+       redef type W: SignupWindow
+
+       init do affected_views.add_all([window.but_login, window.but_signup])
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+
+               if not res isa LoginResult then
+                       on_fail new Error("Server sent unexpected data {res or else "null"}")
+                       return
+               end
+
+               app.token = res.token
+               app.user = res.user.name
+
+               app.on_log_in
+       end
+end
+
+# Async request for signing up
+class SignupAction
+       super WindowHttpRequest
+
+       redef type W: SignupWindow
+
+       init do affected_views.add_all([window.but_signup])
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+
+               if not res isa LoginResult then
+                       on_fail new Error("Server sent unexpected data {res or else "null"}")
+                       return
+               end
+
+               app.token = res.token
+               app.user = res.user.name
+               app.on_log_in
+       end
+end
similarity index 98%
rename from contrib/benitlux/src/benitlux_web.nit
rename to contrib/benitlux/src/server/server.nit
index 3b2743b..f6d5d41 100644 (file)
@@ -15,7 +15,7 @@
 # limitations under the License.
 
 # Web server for Benitlux
-module benitlux_web
+module server
 
 import benitlux_model
 import benitlux_view
diff --git a/contrib/tinks/src/client/tinks_vr.nit b/contrib/tinks/src/client/tinks_vr.nit
new file mode 100644 (file)
index 0000000..0e2a300
--- /dev/null
@@ -0,0 +1,22 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# VR mode for Android with Google Cardboard
+#
+# This version is not playable and very laggy as it is not modified
+# or optimized in any way for VR.
+# This module is made available as a minimal example of a VR game.
+module tinks_vr
+
+import gamnit::vr
index a01d725..4d7720f 100644 (file)
@@ -1,13 +1,13 @@
 [package]
 name=tnitter
-tags=web
+tags=web,mobile
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
 browse=https://github.com/nitlang/nit/tree/master/contrib/tnitter/
 git=https://github.com/nitlang/nit.git
 git.directory=contrib/tnitter/
-homepage=http://nitlanguage.org
+homepage=http://xymus.net/tnitter/
 issues=https://github.com/nitlang/nit/issues
 tryit=http://tnitter.xymus.net/
 apk=http://nitlanguage.org/fdroid/apk/tnitter.apk
index fccad93..7a47218 100644 (file)
@@ -42,7 +42,7 @@ redef class App
        redef fun on_create
        do
                # Create the main window
-               window = new TnitterWindow
+               push_window new TnitterWindow
                super
        end
 end
diff --git a/contrib/xymus_net/.gitignore b/contrib/xymus_net/.gitignore
new file mode 100644 (file)
index 0000000..b2285d4
--- /dev/null
@@ -0,0 +1 @@
+xymus.net
diff --git a/contrib/xymus_net/Makefile b/contrib/xymus_net/Makefile
new file mode 100644 (file)
index 0000000..eff98e3
--- /dev/null
@@ -0,0 +1,9 @@
+all: xymus.net
+
+xymus.net: ../benitlux/src/server/benitlux_restful.nit $(shell ../../bin/nitls -M xymus_net.nit)
+       ../../bin/nitc -o $@ xymus_net.nit
+
+../benitlux/src/server/benitlux_restful.nit:
+       make -C ../benitlux src/server/benitlux_restful.nit
+
+pre-build: ../benitlux/src/server/benitlux_restful.nit
diff --git a/contrib/xymus_net/README.md b/contrib/xymus_net/README.md
new file mode 100644 (file)
index 0000000..5bb2c31
--- /dev/null
@@ -0,0 +1,5 @@
+Web server source and config of xymus.net
+
+This module acts also as an example to merge multiple `nitcorn` projects into one server.
+
+See the server online at http://xymus.net/.
diff --git a/contrib/xymus_net/package.ini b/contrib/xymus_net/package.ini
new file mode 100644 (file)
index 0000000..249a39a
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=xymus_net
+tags=web,example
+maintainer=Alexis Laferrière <alexis.laf@xymus.net>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/contrib/xymus.net/
+git=https://github.com/nitlang/nit.git
+git.directory=contrib/xymus.net/
+homepage=http://xymus.net/
+issues=https://github.com/nitlang/nit/issues
similarity index 96%
rename from lib/nitcorn/examples/src/xymus_net.nit
rename to contrib/xymus_net/xymus_net.nit
index 1233db4..726cb6a 100644 (file)
@@ -1,6 +1,6 @@
 # This file is part of NIT ( http://www.nitlanguage.org ).
 #
-# Copyright 2014-2015 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2014-2016 Alexis Laferrière <alexis.laf@xymus.net>
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import privileges
 # Use actions defined by contribs
 import tnitter
 import benitlux::benitlux_controller
+import benitlux::benitlux_restful
 import opportunity::opportunity_controller
 import nitiwiki::wiki_edit
 
@@ -173,6 +174,7 @@ var vps_vh = new VirtualHost("vps.xymus.net:80")
 var tnitter_vh = new VirtualHost("tnitter.xymus.net:80")
 var pep8_vh = new VirtualHost("pep8.xymus.net:80")
 var benitlux_vh = new VirtualHost("benitlux.xymus.net:80")
+var benitlux_admin_vh = new VirtualHost("localhost:8081")
 
 var factory = new HttpFactory.and_libevent
 factory.config.virtual_hosts.add default_vh
@@ -180,6 +182,7 @@ factory.config.virtual_hosts.add vps_vh
 factory.config.virtual_hosts.add tnitter_vh
 factory.config.virtual_hosts.add pep8_vh
 factory.config.virtual_hosts.add benitlux_vh
+factory.config.virtual_hosts.add benitlux_admin_vh
 
 # Ports are open, drop to a low-privileged user if we are root
 var user_group = new UserGroup("nitcorn", "nitcorn")
@@ -228,6 +231,8 @@ benitlux_vh.routes.add new Route("/push/", benitlux_push)
 benitlux_vh.routes.add new Route("/static/", shared_file_server)
 benitlux_vh.routes.add new Route(null, benitlux_sub)
 
+benitlux_admin_vh.routes.add new Route(null, new BenitluxAdminAction(benitlux_db))
+
 # Opportunity service
 var opportunity = new OpportunityWelcome
 var opportunity_rest = new OpportunityRESTAction
index 80b32cb..8237327 100644 (file)
@@ -1,6 +1,6 @@
 [package]
 name=calculator
-tags=example
+tags=example,mobile
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
index 4068f34..c9db316 100644 (file)
@@ -38,7 +38,7 @@ redef class App
                if debug then print "App::on_create"
 
                # Create the main window
-               window = new CalculatorWindow
+               push_window new CalculatorWindow
                super
        end
 end
index 0733bbf..a5cd314 100644 (file)
@@ -8,69 +8,7 @@
 # See: <http://rosettacode.org/wiki/Perlin_noise>
 module perlin_noise
 
-redef universal Float
-       # Smoothened `self`
-       fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0)
-end
-
-# Improved noise
-class ImprovedNoise
-       # Permutations
-       var p: Array[Int] = [151,160,137,91,90,15,
-               131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
-               190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
-               88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
-               77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
-               102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
-               135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
-               5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
-               223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
-               129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
-               251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
-               49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
-               138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]
-
-       # Noise value in [-1..1] at 3d coordinates `x, y, z`
-       fun noise(x, y, z: Float): Float
-       do
-               var xx = x.to_i & 255
-               var yy = y.to_i & 255
-               var zz = z.to_i & 255
-
-               x -= x.floor
-               y -= y.floor
-               z -= z.floor
-
-               var u = x.fade
-               var v = y.fade
-               var w = z.fade
-
-               var a  = p[xx  ] + yy
-               var aa = p[a   ] + zz
-               var ab = p[a+1 ] + zz
-               var b  = p[xx+1] + yy
-               var ba = p[b   ] + zz
-               var bb = p[b+1 ] + zz
-
-               return w.lerp(v.lerp(u.lerp(grad(p[aa  ], x,     y,     z    ),
-                                           grad(p[ba  ], x-1.0, y,     z    )),
-                                    u.lerp(grad(p[ab  ], x,     y-1.0, z    ),
-                                           grad(p[bb  ], x-1.0, y-1.0, z    ))),
-                      v.lerp(u.lerp(grad(p[aa+1], x,     y,     z-1.0),
-                                           grad(p[ba+1], x-1.0, y,     z-1.0)),
-                                    u.lerp(grad(p[ab+1], x,     y-1.0, z-1.0),
-                                           grad(p[bb+1], x-1.0, y-1.0, z-1.0))))
-       end
-
-       # Value at a corner of the grid
-       fun grad(hash: Int, x, y, z: Float): Float
-       do
-               var h = hash & 15
-               var u = if h < 8 then x else y
-               var v = if h < 4 then y else if h == 12 or h == 14 then x else z
-               return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v)
-       end
-end
+import noise
 
 var map = new ImprovedNoise
 print map.noise(3.14, 42.0, 7.0).to_precision(17)
index 7b7ce02..cc0cba3 100644 (file)
@@ -188,6 +188,14 @@ class Node
                end
        end
 
+       # Find the closest node accepted by `cond` under `max_cost`
+       fun find_closest(max_cost: Int, context: PathContext, cond: nullable TargetCondition[N]): nullable N
+       do
+               var path = path_to_alts(null, max_cost, context, cond)
+               if path == null then return null
+               return path.nodes.last
+       end
+
        # We customize the serialization process to avoid problems with recursive
        # serialization engines. These engines, such as `JsonSerializer`,
        # are at danger to serialize the graph as a very deep tree.
index 3744988..cbe6ad4 100644 (file)
@@ -17,6 +17,7 @@ package nit.app;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.view.KeyEvent;
 
 /*
  * Entry point to Nit applications on Android, redirect most calls to Nit
@@ -47,6 +48,11 @@ public class NitActivity extends Activity {
        protected native void nitOnDestroy(int activity);
        protected native void nitOnSaveInstanceState(int activity, Bundle savedInstanceState);
        protected native void nitOnRestoreInstanceState(int activity, Bundle savedInstanceState);
+       protected native boolean nitOnBackPressed(int activity);
+       protected native boolean nitOnKeyDown(int activity, int keyCode, KeyEvent event);
+       protected native boolean nitOnKeyLongPress(int activity, int keyCode, KeyEvent event);
+       protected native boolean nitOnKeyMultiple(int activity, int keyCode, int count, KeyEvent event);
+       protected native boolean nitOnKeyUp(int activity, int keyCode, KeyEvent event);
 
        /*
         * Implementation of OS callbacks
@@ -108,4 +114,34 @@ public class NitActivity extends Activity {
                super.onRestoreInstanceState(savedInstanceState);
                nitOnRestoreInstanceState(nitActivity, savedInstanceState);
        }
+
+       @Override
+       public void onBackPressed() {
+               if (!nitOnBackPressed(nitActivity))
+                       super.onBackPressed();
+       }
+
+       @Override
+       public boolean onKeyDown(int keyCode, KeyEvent event) {
+               return nitOnKeyDown(nitActivity, keyCode, event)
+                       || super.onKeyDown(keyCode, event);
+       }
+
+       @Override
+       public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+               return nitOnKeyLongPress(nitActivity, keyCode, event)
+                       || super.onKeyLongPress(keyCode, event);
+       }
+
+       @Override
+       public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+               return nitOnKeyMultiple(nitActivity, keyCode, count, event)
+                       || super.onKeyMultiple(keyCode, count, event);
+       }
+
+       @Override
+       public boolean onKeyUp(int keyCode, KeyEvent event) {
+               return nitOnKeyUp(nitActivity, keyCode, event)
+                       || super.onKeyUp(keyCode, event);
+       }
 }
index 8aea378..5ee9d87 100644 (file)
@@ -70,6 +70,8 @@ private extern class NativeAndroidMotionEvent `{AInputEvent *`}
        `}
 
        fun action: AMotionEventAction `{ return AMotionEvent_getAction(self); `}
+
+       fun native_down_time: Int `{ return AMotionEvent_getDownTime(self); `}
 end
 
 private extern class AMotionEventAction `{ int32_t `}
@@ -149,6 +151,11 @@ class AndroidMotionEvent
                        return null
                end
        end
+
+       # Time when the user originally pressed down to start a stream of position events
+       #
+       # The return value is in the `java.lang.System.nanoTime()` time base.
+       fun down_time: Int do return native.native_down_time
 end
 
 # A pointer event
diff --git a/lib/android/key_event.nit b/lib/android/key_event.nit
new file mode 100644 (file)
index 0000000..b5b39e4
--- /dev/null
@@ -0,0 +1,192 @@
+# 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.
+
+module key_event
+
+import platform
+
+# Java class: android.view.KeyEvent
+extern class NativeKeyEvent in "Java" `{ android.view.KeyEvent `}
+       super JavaObject
+
+       # Java implementation: boolean android.view.KeyEvent.isSystem()
+       fun is_system: Bool in "Java" `{
+               return self.isSystem();
+       `}
+
+       # Java implementation:  android.view.KeyEvent.setSource(int)
+       fun set_source(arg0: Int) in "Java" `{
+               self.setSource((int)arg0);
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getMetaState()
+       fun meta_state: Int in "Java" `{
+               return self.getMetaState();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getModifiers()
+       fun modifiers: Int in "Java" `{
+               return self.getModifiers();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getFlags()
+       fun flags: Int in "Java" `{
+               return self.getFlags();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.hasNoModifiers()
+       fun has_no_modifiers: Bool in "Java" `{
+               return self.hasNoModifiers();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.hasModifiers(int)
+       fun has_modifiers(arg0: Int): Bool in "Java" `{
+               return self.hasModifiers((int)arg0);
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isAltPressed()
+       fun is_alt_pressed: Bool in "Java" `{
+               return self.isAltPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isShiftPressed()
+       fun is_shift_pressed: Bool in "Java" `{
+               return self.isShiftPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isSymPressed()
+       fun is_sym_pressed: Bool in "Java" `{
+               return self.isSymPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCtrlPressed()
+       fun is_ctrl_pressed: Bool in "Java" `{
+               return self.isCtrlPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isMetaPressed()
+       fun is_meta_pressed: Bool in "Java" `{
+               return self.isMetaPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isFunctionPressed()
+       fun is_function_pressed: Bool in "Java" `{
+               return self.isFunctionPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCapsLockOn()
+       fun is_caps_lock_on: Bool in "Java" `{
+               return self.isCapsLockOn();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isNumLockOn()
+       fun is_num_lock_on: Bool in "Java" `{
+               return self.isNumLockOn();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isScrollLockOn()
+       fun is_scroll_lock_on: Bool in "Java" `{
+               return self.isScrollLockOn();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getAction()
+       fun action: Int in "Java" `{
+               return self.getAction();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCanceled()
+       fun is_canceled: Bool in "Java" `{
+               return self.isCanceled();
+       `}
+
+       # Java implementation:  android.view.KeyEvent.startTracking()
+       fun start_tracking in "Java" `{
+               self.startTracking();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isTracking()
+       fun is_tracking: Bool in "Java" `{
+               return self.isTracking();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isLongPress()
+       fun is_long_press: Bool in "Java" `{
+               return self.isLongPress();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getKeyCode()
+       fun key_code: Int in "Java" `{
+               return self.getKeyCode();
+       `}
+
+       # Java implementation: java.lang.String android.view.KeyEvent.getCharacters()
+       fun characters: JavaString in "Java" `{
+               return self.getCharacters();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getScanCode()
+       fun scan_code: Int in "Java" `{
+               return self.getScanCode();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getRepeatCount()
+       fun repeat_count: Int in "Java" `{
+               return self.getRepeatCount();
+       `}
+
+       # Java implementation: long android.view.KeyEvent.getDownTime()
+       fun down_time: Int in "Java" `{
+               return self.getDownTime();
+       `}
+
+       # Java implementation: long android.view.KeyEvent.getEventTime()
+       fun event_time: Int in "Java" `{
+               return self.getEventTime();
+       `}
+
+       # Java implementation: char android.view.KeyEvent.getDisplayLabel()
+       fun display_label: Char in "Java" `{
+               return self.getDisplayLabel();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getUnicodeChar()
+       fun unicode_char: Int in "Java" `{
+               return self.getUnicodeChar();
+       `}
+
+       # Java implementation: char android.view.KeyEvent.getNumber()
+       fun number: Char in "Java" `{
+               return self.getNumber();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isPrintingKey()
+       fun is_printing_key: Bool in "Java" `{
+               return self.isPrintingKey();
+       `}
+
+       redef fun new_global_ref import sys, Sys.jni_env `{
+               Sys sys = NativeKeyEvent_sys(self);
+               JNIEnv *env = Sys_jni_env(sys);
+               return (*env)->NewGlobalRef(env, self);
+       `}
+
+       redef fun pop_from_local_frame_with_env(jni_env) `{
+               return (*jni_env)->PopLocalFrame(jni_env, self);
+       `}
+end
+
+# Java getter: android.view.KeyEvent.KEYCODE_BACK
+fun android_view_key_event_keycode_back: Int in "Java" `{
+       return android.view.KeyEvent.KEYCODE_BACK;
+`}
index 87d9d22..1668147 100644 (file)
@@ -43,6 +43,7 @@ end
 import platform
 import log
 import activities
+import key_event
 import bundle
 import dalvik
 
@@ -133,6 +134,36 @@ in "C body" `{
        {
                Activity_on_restore_instance_state((Activity)nit_activity, saved_state);
        }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnBackPressed
+         (JNIEnv *env, jobject java_activity, jint nit_activity)
+       {
+               return (jboolean)Activity_on_back_pressed((Activity)nit_activity);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyDown
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_down((Activity)nit_activity, keyCode, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyLongPress
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_long_press((Activity)nit_activity, keyCode, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyMultiple
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jint count, jobject event)
+       {
+               return (jboolean)Activity_on_key_multiple((Activity)nit_activity, keyCode, count, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyUp
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_up((Activity)nit_activity, keyCode, event);
+       }
 `}
 
 # Wrapper to our Java `NitActivity`
@@ -158,7 +189,10 @@ redef class App
        Activity.on_create, Activity.on_destroy,
        Activity.on_start, Activity.on_restart, Activity.on_stop,
        Activity.on_pause, Activity.on_resume,
-       Activity.on_save_instance_state, Activity.on_restore_instance_state `{
+       Activity.on_save_instance_state, Activity.on_restore_instance_state,
+       Activity.on_back_pressed,
+       Activity.on_key_down, Activity.on_key_long_press,
+       Activity.on_key_multiple, Activity.on_key_up `{
                App_incr_ref(self);
                global_app = self;
        `}
@@ -251,6 +285,31 @@ class Activity
 
        # Notification from Android, the current device configuration has changed
        fun on_configuration_changed do end
+
+       # The back key has been pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_back_pressed: Bool do return false
+
+       # A key has been pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_down(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+       # A key has been long pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_long_press(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+       # Multiple down/up pairs of the same key have occurred in a row
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_multiple(key_code, count: Int, event: NativeKeyEvent): Bool do return false
+
+       # A key has been released
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_up(key_code: Int, event: NativeKeyEvent): Bool do return false
 end
 
 # Set up global data in C and leave it to Android to callback Java, which we relay to Nit
index 65535e8..cd9f4ea 100644 (file)
 # limitations under the License.
 
 # Views and services to use the Android native user interface
-module ui
+module ui is
+       # `adjustPan` allows to use EditText in a ListLayout
+       android_manifest_activity """android:windowSoftInputMode="adjustPan""""
+end
 
 # Implementation note:
 #
@@ -78,6 +81,19 @@ redef class App
        end
 end
 
+redef class Activity
+       redef fun on_back_pressed
+       do
+               var window = app.window
+               if window.enable_back_button then
+                       window.on_back_button
+                       return true
+               end
+
+               return false
+       end
+end
+
 # On Android, a window is implemented with the fragment `native`
 redef class Window
        redef var native = (new Android_app_Fragment(self)).new_global_ref
@@ -228,7 +244,7 @@ redef class TextView
                else // if (align > 0.5d)
                        g = android.view.Gravity.RIGHT;
 
-               view.setGravity(g);
+               view.setGravity(g | android.view.Gravity.CENTER_VERTICAL);
        `}
 end
 
@@ -240,9 +256,26 @@ end
 redef class CheckBox
        redef type NATIVE: Android_widget_CompoundButton
        redef var native do return (new Android_widget_CheckBox(app.native_activity)).new_global_ref
+       init do set_callback_on_toggle(native)
 
        redef fun is_checked do return native.is_checked
        redef fun is_checked=(value) do native.set_checked(value)
+
+       private fun on_toggle do notify_observers new ToggleEvent(self)
+
+       private fun set_callback_on_toggle(view: NATIVE)
+       import on_toggle in "Java" `{
+               final int final_sender_object = self;
+               CheckBox_incr_ref(final_sender_object);
+
+               view.setOnCheckedChangeListener(
+                       new android.widget.CompoundButton.OnCheckedChangeListener() {
+                               @Override
+                               public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
+                                       CheckBox_on_toggle(final_sender_object);
+                               }
+                       });
+       `}
 end
 
 redef class TextInput
index 0c7d524..0afe7aa 100644 (file)
@@ -27,8 +27,32 @@ redef class App
 
        # The current `Window` of this activity
        #
-       # This attribute must be set by refinements of `App`.
-       var window: Window is writable
+       # This attribute is set by `push_window`.
+       var window: Window is noinit
+
+       # Make visible and push `window` on the top of `pop_window`
+       #
+       # This method must be called at least once within `App::on_create`.
+       # It can be called at any times while the app is active.
+       fun push_window(window: Window)
+       do
+               window_stack.add window
+               self.window = window
+       end
+
+       # Pop the current `window` from the stack and show the previous one
+       #
+       # Require: `window_stack.not_empty`
+       fun pop_window
+       do
+               assert window_stack.not_empty
+               window_stack.pop
+               window = window_stack.last
+               window.on_resume
+       end
+
+       # Stack of active windows
+       var window_stack = new Array[Window]
 
        redef fun on_create do window.on_create
 
@@ -140,6 +164,12 @@ end
 # A window, root of the `Control` tree
 class Window
        super CompositeControl
+
+       # Should the back button be shown and used to go back to a previous window?
+       fun enable_back_button: Bool do return app.window_stack.length > 1
+
+       # The back button has been pressed, usually to open the previous window
+       fun on_back_button do app.pop_window
 end
 
 # A viewable `Control`
@@ -210,12 +240,29 @@ class CheckBox
        var is_checked = false is writable
 end
 
+# Event sent from a `VIEW`
+class ViewEvent
+       super AppEvent
+
+       # The `VIEW` that raised this event
+       var sender: VIEW
+
+       # Type of the `sender`
+       type VIEW: View
+end
+
 # A `Button` press event
 class ButtonPressEvent
-       super AppEvent
+       super ViewEvent
+
+       redef type VIEW: Button
+end
+
+# The `CheckBox` `sender` has been toggled
+class ToggleEvent
+       super ViewEvent
 
-       # The `Button` that raised this event
-       var sender: Button
+       redef type VIEW: CheckBox
 end
 
 # A layout to visually organize `Control`s
index b5542e4..28129a5 100644 (file)
@@ -26,6 +26,7 @@
 module bucketed_game is serialize
 
 import serialization
+import counter
 
 # Something acting on the game
 abstract class Turnable[G: Game]
@@ -65,6 +66,9 @@ class Buckets[G: Game]
        private var buckets: Array[BUCKET] =
                [for b in n_buckets.times do new HashSet[Bucketable[G]]] is lazy
 
+       # Stats on delays asked when adding an event with `act_in` and `act_next`
+       private var delays = new Counter[Int]
+
        # Add the Bucketable event `e` at `at_tick`.
        fun add_at(e: Bucketable[G], at_tick: Int)
        do
@@ -106,6 +110,26 @@ class Buckets[G: Game]
                        end
                end
        end
+
+       # Get some statistics on both the current held events and historic expired events
+       fun stats: String
+       do
+               var entries = 0
+               var instances = new HashSet[Bucketable[G]]
+               var max = 0
+               var min = 100000
+               for bucket in buckets do
+                       var len = bucket.length
+                       entries += len
+                       instances.add_all bucket
+                       min = min.min(len)
+                       max = max.max(len)
+               end
+               var avg = entries.to_f / buckets.length.to_f
+
+               return "{buckets.length} buckets; uniq/tot:{instances.length}/{entries}, avg:{avg.to_precision(1)}, min:{min}, max:{max}\n" +
+                       "history:{delays.sum}, avg:{delays.avg}, min:{delays[delays.min.as(not null)]}, max:{delays[delays.max.as(not null)]}"
+       end
 end
 
 # Game related event
@@ -123,16 +147,16 @@ end
 # Game logic on the client
 class ThinGame
 
-       # Game tick when `self` should act.
+       # Current game tick
        #
        # Default is 0.
-       var tick: Int = 0 is protected writable
+       var tick: Int = 0 is writable
 end
 
 # Game turn on the client
 class ThinGameTurn[G: ThinGame]
 
-       # Game tick when `self` should act.
+       # Game tick when happened this turn
        var tick: Int is protected writable
 
        # Game events occurred for `self`.
@@ -153,10 +177,18 @@ class GameTurn[G: Game]
        end
 
        # Insert the Bucketable event `e` to be executed at next tick.
-       fun act_next(e: Bucketable[G]) do game.buckets.add_at(e, tick + 1)
+       fun act_next(e: Bucketable[G])
+       do
+               game.buckets.add_at(e, tick + 1)
+               game.buckets.delays.inc(1)
+       end
 
        # Insert the Bucketable event `e` to be executed at tick `t`.
-       fun act_in(e: Bucketable[G], t: Int) do game.buckets.add_at(e, tick + t)
+       fun act_in(e: Bucketable[G], t: Int)
+       do
+               game.buckets.add_at(e, tick + t)
+               game.buckets.delays.inc(t)
+       end
 
        # Add and `apply` a game `event`.
        fun add_event( event : GameEvent )
index b7e8e61..2fa0bcb 100644 (file)
@@ -666,15 +666,15 @@ private class ASCIIFlatString
        end
 
        redef fun substring(from, count) do
+               var ln = _length
+               if count <= 0 then return ""
+               if (count + from) > ln then count = ln - from
                if count <= 0 then return ""
-
                if from < 0 then
                        count += from
-                       if count < 0 then return ""
+                       if count <= 0 then return ""
                        from = 0
                end
-               var ln = _length
-               if (count + from) > ln then count = ln - from
                return new ASCIIFlatString.full_data(_items, count, from + _first_byte, count)
        end
 
index a1eed2f..245d071 100644 (file)
@@ -162,6 +162,35 @@ class EulerCamera
                yaw = 0.0
                roll = 0.0
        end
+
+       # Convert the position `x, y` on screen, to world coordinates on the plane at `target_z`
+       #
+       # `target_z` defaults to `0.0` and specifies the Z coordinates of the plane
+       # on which to project the screen position `x, y`.
+       #
+       # This method assumes that the camera is looking along the Z axis towards higher values.
+       # Using it in a different orientation can be useful, but won't result in valid
+       # world coordinates.
+       fun camera_to_world(x, y: Numeric, target_z: nullable Float): Point[Float]
+       do
+               # TODO, this method could be tweaked to support projecting the 2D point,
+               # on the near plane (x,y) onto a given distance no matter to orientation
+               # of the camera.
+
+               target_z = target_z or else 0.0
+
+               # Convert from pixel units / window resolution to
+               # units on the near clipping wall to
+               # units on the target wall at Z = 0
+               var near_height = (field_of_view_y/2.0).tan * near
+               var cross_screen_to_near = near_height / (display.height.to_f/2.0)
+               var cross_near_to_target = (position.z - target_z) / near
+               var mod = cross_screen_to_near * cross_near_to_target * 1.72 # FIXME drop the magic number
+
+               var wx = position.x + (x.to_f-display.width.to_f/2.0) * mod
+               var wy = position.y - (y.to_f-display.height.to_f/2.0) * mod
+               return new Point[Float](wx, wy)
+       end
 end
 
 # Orthogonal camera to draw UI objects with services to work with screens of different sizes
index 03a3970..f1e5803 100644 (file)
@@ -103,6 +103,9 @@ class Mesh
        # Coordinates on the texture per vertex
        var texture_coords = new Array[Float] is lazy, writable
 
+       # `GLDrawMode` used to display this mesh, defaults to `gl_TRIANGLES`
+       fun draw_mode: GLDrawMode do return gl_TRIANGLES
+
        # Create an UV sphere of `radius` with `n_meridians` and `n_parallels`
        init uv_sphere(radius: Float, n_meridians, n_parallels: Int)
        do
index 613ff1a..b77c85e 100644 (file)
@@ -75,9 +75,9 @@ class SmoothMaterial
 
                # Execute draw
                if mesh.indices.is_empty then
-                       glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
                else
-                       glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
 end
@@ -154,11 +154,13 @@ class TexturedMaterial
                                var xd = sample_used_texture.offset_right - xa
                                var ya = sample_used_texture.offset_top
                                var yd = sample_used_texture.offset_bottom - ya
+                               xd *= 0.999
+                               yd *= 0.999
 
                                var tex_coords = new Array[Float].with_capacity(mesh.texture_coords.length)
                                for i in [0..mesh.texture_coords.length/2[ do
                                        tex_coords[i*2]   = xa + xd * mesh.texture_coords[i*2]
-                                       tex_coords[i*2+1] = ya + yd * mesh.texture_coords[i*2+1]
+                                       tex_coords[i*2+1] = 1.0 - (ya + yd * mesh.texture_coords[i*2+1])
                                end
 
                                program.tex_coord.array(tex_coords, 2)
@@ -180,9 +182,9 @@ class TexturedMaterial
                program.camera.uniform(app.world_camera.position.x, app.world_camera.position.y, app.world_camera.position.z)
 
                if mesh.indices.is_empty then
-                       glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
                else
-                       glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
 end
@@ -218,9 +220,9 @@ class NormalsMaterial
                program.normal.array(mesh.normals, 3)
 
                if mesh.indices.is_empty then
-                       glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
                else
-                       glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
 end
index 9f2318a..569973b 100644 (file)
@@ -124,7 +124,8 @@ class Cube
                var d = [1.0, 0.0]
 
                var texture_coords = new Array[Float]
-               for v in [c, d, a, a, d, b] do for i in 6.times do texture_coords.add_all v
+               var face = [a, c, d, a, d, b]
+               for i in 6.times do for v in face do texture_coords.add_all v
                return texture_coords
        end
 
index a126223..c2b9664 100644 (file)
@@ -50,7 +50,7 @@ class ModelAsset
 
                if leaves.is_empty then
                        # Nothing was loaded, use a cube with the default material
-                       var leaf = new LeafModel(new Cube, new SmoothMaterial.default)
+                       var leaf = placeholder_model
                        leaves.add leaf
                end
        end
@@ -168,7 +168,7 @@ private class ModelFromObj
                # Load textures need for these materials
                for name in texture_names do
                        if not asset_textures_by_name.keys.has(name) then
-                               var tex = new GamnitAssetTexture(name)
+                               var tex = new TextureAsset(name)
                                asset_textures_by_name[name] = tex
                        end
                end
@@ -379,8 +379,13 @@ end
 
 redef class Sys
        # Textures loaded from .mtl files for models
-       var asset_textures_by_name = new Map[String, GamnitAssetTexture]
+       var asset_textures_by_name = new Map[String, TextureAsset]
 
        # All instantiated asset models
        var models = new Set[ModelAsset]
+
+       # Blue cube of 1 unit on each side, acting as placeholder for models failing to load
+       #
+       # This model can be freely used by any `Actor` as placeholder or for debugging.
+       var placeholder_model = new LeafModel(new Cube, new SmoothMaterial.default) is lazy
 end
index 69ddb41..6e7c58c 100644 (file)
@@ -44,6 +44,15 @@ class GamnitDisplay
        # Only affects the desktop implementations.
        var show_cursor: Bool = true is writable
 
+       # Number of bits used for the red value in the color buffer
+       fun red_bits: Int do return 8
+
+       # Number of bits used for the green value in the color buffer
+       fun green_bits: Int do return 8
+
+       # Number of bits used for the blue value in the color buffer
+       fun blue_bits: Int do return 8
+
        # Prepare this display
        #
        # The implementation varies per platform.
index c412e9f..e9d1b4a 100644 (file)
@@ -37,7 +37,7 @@ redef class GamnitDisplay
                setup_egl_display native_display
 
                # We need 8 bits per color for selection by color
-               select_egl_config(8, 8, 8, 0, 8, 0, 0)
+               select_egl_config(red_bits, green_bits, blue_bits, 0, 8, 0, 0)
 
                var format = egl_config.attribs(egl_display).native_visual_id
                native_window.set_buffers_geometry(0, 0, format)
@@ -48,7 +48,7 @@ redef class GamnitDisplay
        redef fun close do close_egl
 end
 
-redef class GamnitAssetTexture
+redef class TextureAsset
 
        redef fun load_from_platform
        do
index 8f115e7..1dbee21 100644 (file)
@@ -48,7 +48,7 @@ redef class GamnitDisplay
                setup_egl_display x11_display
 
                if debug_gamnit then print "Setting up EGL context"
-               select_egl_config(8, 8, 8, 8, 8, 0, 0)
+               select_egl_config(red_bits, green_bits, blue_bits, 8, 8, 0, 0)
                setup_egl_context window_handle
        end
 
@@ -95,7 +95,7 @@ redef class GamnitDisplay
        end
 end
 
-redef class GamnitAssetTexture
+redef class TextureAsset
 
        redef fun load_from_platform
        do
index 3fcb8c8..b451362 100644 (file)
@@ -46,14 +46,14 @@ redef class GamnitDisplay
        end
 
        # Select an EGL config
-       protected fun select_egl_config(blue, green, red, alpha, depth, stencil, sample: Int)
+       protected fun select_egl_config(red, green, blue, alpha, depth, stencil, sample: Int)
        do
                var config_chooser = new EGLConfigChooser
                config_chooser.renderable_type_egl
                config_chooser.surface_type_egl
-               config_chooser.blue_size = blue
-               config_chooser.green_size = green
                config_chooser.red_size = red
+               config_chooser.green_size = green
+               config_chooser.blue_size = blue
                if alpha > 0 then config_chooser.alpha_size = alpha
                if depth > 0 then config_chooser.depth_size = depth
                if stencil > 0 then config_chooser.stencil_size = stencil
index a300b54..f16609f 100644 (file)
@@ -29,8 +29,8 @@ redef class App
 
        # Current frame-rate
        #
-       # Updated each 5 seconds.
-       var current_fps = 0.0
+       # Updated each 5 seconds, initialized at the value of `maximum_fps`.
+       var current_fps: Float = maximum_fps is lazy
 
        redef fun frame_full
        do
@@ -47,7 +47,7 @@ redef class App
        private var frame_count = 0
 
        # Deadline used to compute `current_fps`
-       private var frame_count_deadline = 0
+       private var frame_count_deadline = 5
 
        # Check and sleep to maintain a frame-rate bellow `maximum_fps`
        #
index 1a995ef..6859c76 100644 (file)
@@ -428,6 +428,23 @@ abstract class GamnitProgram
                end
        end
 
+       # Diagnose possible problems with the shaders of the program
+       #
+       # Lists to the console inactive uniforms and attributes.
+       # These may not be problematic but they can help to debug the program.
+       fun diagnose
+       do
+               if gl_program == null then compile_and_link
+
+               print "# Diagnose {class_name}"
+               for k,v in uniforms do
+                       if not v.is_active then print "* Uniform {v.name} is inactive"
+               end
+               for k,v in attributes do
+                       if not v.is_active then print "* Attribute {v.name} is inactive"
+               end
+       end
+
        # Attributes of this program organized by name
        #
        # Active attributes are gathered at `compile_and_link`.
similarity index 98%
rename from contrib/asteronits/src/texture_atlas_parser.nit
rename to lib/gamnit/texture_atlas_parser.nit
index 113b887..a0e4d5f 100644 (file)
@@ -44,7 +44,7 @@ var attributes = new Array[String]
 # Insert the first attribute, to load the root texture
 var png_file = "images" / xml_file.basename("xml") + "png"
 attributes.add """
-       var root_texture = new Texture("{{{png_file}}}")"""
+       var root_texture = new TextureAsset("{{{png_file}}}")"""
 
 # Read XML file
 var content = xml_file.to_path.read_all
index 415aa90..d644d60 100644 (file)
@@ -21,7 +21,7 @@ import display
 abstract class Texture
 
        # Prepare a texture located at `path` within the `assets` folder
-       new (path: Text) do return new GamnitAssetTexture(path.to_s)
+       new (path: Text) do return new TextureAsset(path.to_s)
 
        # Root texture of which `self` is derived
        fun root: GamnitRootTexture is abstract
@@ -41,7 +41,7 @@ abstract class Texture
        # OpenGL handle to this texture
        fun gl_texture: Int do return root.gl_texture
 
-       # Prepare a subtexture from this texture
+       # Prepare a subtexture from this texture, from the given pixel offsets
        fun subtexture(left, top, width, height: Numeric): GamnitSubtexture
        do
                # Setup the subtexture
@@ -143,7 +143,7 @@ class GamnitRootTexture
 end
 
 # Texture loaded from the assets folder
-class GamnitAssetTexture
+class TextureAsset
        super GamnitRootTexture
 
        # Path to this texture within the `assets` folder
index 94ad1c9..1be832e 100644 (file)
@@ -441,31 +441,40 @@ fun turn_left(p1, p2, p3: Point[Float]): Bool do
 end
 
 # Split a polygon into triangles
-# Useful for converting a concave polygon into multiple convex ones
-fun triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do
-       var poly = new Polygon(pts)
-       pts = poly.points
-       recursive_triangulate(pts, results)
+#
+# Useful for converting a concave polygon into multiple convex ones.
+#
+# See: the alternative `triangulate_recursive` uses arrays in-place.
+fun triangulate(points: Array[Point[Float]]): Array[ConvexPolygon]
+do
+       var results = new Array[ConvexPolygon]
+       triangulate_recursive(points.clone, results)
+       return results
 end
 
-private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do
-       if pts.length == 3 then
-               results.add(new ConvexPolygon(pts))
+# Split a polygon into triangles using arrays in-place
+#
+# Consumes the content of `points` and add the triangles to `results`.
+#
+# See: the alternative `triangulate` which does not modify `points` and returns a new array.
+fun triangulate_recursive(points: Array[Point[Float]], results: Array[ConvexPolygon]) do
+       if points.length == 3 then
+               results.add(new ConvexPolygon(points))
                return
        end
-       var prev = pts[pts.length - 1]
-       var curr = pts[0]
-       var next = pts[1]
-       for i in [1..pts.length[ do
+       var prev = points[points.length - 1]
+       var curr = points[0]
+       var next = points[1]
+       for i in [1..points.length[ do
                if turn_left(prev, curr, next) then
-                       prev = pts[i-1]
+                       prev = points[i-1]
                        curr = next
-                       if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1]
+                       if i+1 == points.length then next = points[points.length - 1] else next = points[i+1]
                        continue
                end
                var contains = false
                var triangle = new ConvexPolygon(new Array[Point[Float]].with_items(prev, curr, next))
-               for p in pts do
+               for p in points do
                        if p != prev and p != curr and p != next then
                                if triangle.contain(p) then
                                        contains = true
@@ -475,12 +484,12 @@ private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[Conve
                end
                if not contains then
                        results.add(triangle)
-                       pts.remove(curr)
-                       recursive_triangulate(pts, results)
+                       points.remove(curr)
+                       triangulate_recursive(points, results)
                        break
                end
-               prev = pts[i-1]
+               prev = points[i-1]
                curr = next
-               if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1]
+               if i+1 == points.length then next = points[points.length - 1] else next = points[i+1]
        end
 end
index a4a0000..48c7e12 100644 (file)
@@ -25,16 +25,16 @@ in "ObjC" `{
 @interface NitCallbackReference: NSObject
 
        // Nit object target of the callbacks from UI events
-       @property (nonatomic) Button nit_button;
+       @property (nonatomic) View nit_view;
 
        // Actual callback method
-       -(void) nitOnEvent: (UIButton*) sender;
+       -(void) nitOnEvent: (UIView*) sender;
 @end
 
 @implementation NitCallbackReference
 
-       -(void) nitOnEvent: (UIButton*) sender {
-               Button_on_click(self.nit_button);
+       -(void) nitOnEvent: (UIView*) sender {
+               View_on_ios_event(self.nit_view);
        }
 @end
 
@@ -112,6 +112,19 @@ redef class App
                set_view_controller(app_delegate.window, window.native)
                super
        end
+
+       # Use iOS ` popViewControllerAnimated`
+       redef fun pop_window
+       do
+               window_stack.pop
+               pop_view_controller app_delegate.window
+               window.on_resume
+       end
+
+       private fun pop_view_controller(window: UIWindow) in "ObjC" `{
+               UINavigationController *navController = (UINavigationController*)window.rootViewController;
+               [navController popViewControllerAnimated: YES];
+       `}
 end
 
 redef class AppDelegate
@@ -136,6 +149,8 @@ redef class View
        redef type NATIVE: UIView
 
        redef var enabled = null is lazy
+
+       private fun on_ios_event do end
 end
 
 redef class CompositeControl
@@ -180,7 +195,6 @@ redef class Layout
        init
        do
                native.alignment = new UIStackViewAlignment.fill
-               native.distribution = new UIStackViewDistribution.fill_equally
 
                # TODO make customizable
                native.spacing = 4.0
@@ -197,11 +211,19 @@ redef class Layout
 end
 
 redef class HorizontalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.horizontal
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.horizontal
+               native.distribution = new UIStackViewDistribution.fill_equally
+       end
 end
 
 redef class VerticalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.vertical
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.vertical
+               native.distribution = new UIStackViewDistribution.equal_spacing
+       end
 end
 
 redef class Label
@@ -253,15 +275,20 @@ redef class CheckBox
        # `UISwitch` acting as the real check box
        var ui_switch: UISwitch is noautoinit
 
-       init do
+       redef fun on_ios_event do notify_observers new ToggleEvent(self)
+
+       init
+       do
                # Tweak the layout so it is centered
-               layout.native.distribution = new UIStackViewDistribution.fill_proportionally
-               layout.native.alignment = new UIStackViewAlignment.center
+               layout.native.distribution = new UIStackViewDistribution.equal_spacing
+               layout.native.alignment = new UIStackViewAlignment.fill
                layout.native.layout_margins_relative_arrangement = true
 
                var s = new UISwitch
                native.add_arranged_subview s
                ui_switch = s
+
+               ui_switch.set_callback self
        end
 
        redef fun text=(text) do lbl.text = text
@@ -271,6 +298,23 @@ redef class CheckBox
        redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
 end
 
+redef class UISwitch
+       # Register callbacks on this switch to be relayed to `sender`
+       private fun set_callback(sender: View)
+       import View.on_ios_event in "ObjC" `{
+
+               NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
+               ncr.nit_view = sender;
+
+               // Pin the objects in both Objective-C and Nit GC
+               View_incr_ref(sender);
+               ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
+
+               [self addTarget:ncr action:@selector(nitOnEvent:)
+                       forControlEvents:UIControlEventValueChanged];
+       `}
+end
+
 redef class TextInput
 
        redef type NATIVE: UITextField
@@ -293,25 +337,25 @@ redef class Button
 
        init do native.set_callback self
 
+       redef fun on_ios_event do notify_observers new ButtonPressEvent(self)
+
        redef fun text=(text) do if text != null then native.title = text.to_nsstring
        redef fun text do return native.current_title.to_s
 
-       private fun on_click do notify_observers new ButtonPressEvent(self)
-
        redef fun enabled=(enabled) do native.enabled = enabled or else true
        redef fun enabled do return native.enabled
 end
 
 redef class UIButton
        # Register callbacks on this button to be relayed to `sender`
-       private fun set_callback(sender: Button)
-       import Button.on_click in "ObjC" `{
+       private fun set_callback(sender: View)
+       import View.on_ios_event in "ObjC" `{
 
                NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
-               ncr.nit_button = sender;
+               ncr.nit_view = sender;
 
                // Pin the objects in both Objective-C and Nit GC
-               Button_incr_ref(sender);
+               View_incr_ref(sender);
                ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
 
                [self addTarget:ncr action:@selector(nitOnEvent:)
@@ -333,7 +377,7 @@ redef class ListLayout
                native_stack_view.translates_autoresizing_mask_into_constraits = false
                native_stack_view.axis = new UILayoutConstraintAxis.vertical
                native_stack_view.alignment = new UIStackViewAlignment.fill
-               native_stack_view.distribution = new UIStackViewDistribution.fill_equally
+               native_stack_view.distribution = new UIStackViewDistribution.equal_spacing
                native_stack_view.spacing = 4.0
 
                native.add_subview native_stack_view
index ca004a0..1e26f17 100644 (file)
@@ -43,7 +43,7 @@ redef class App
                bar.title = "app.nit" # TODO offer a portable API to name windows
                bar.show_close_button = true
 
-               # TODO add back button
+               bar.add back_button.native
 
                return bar
        end
@@ -55,6 +55,9 @@ redef class App
                return stack
        end
 
+       # Button on the header bar to go back
+       var back_button = new BackButton is lazy
+
        # On GNU/Linux, we go through all the callbacks once,
        # there is no complex life-cycle.
        redef fun run
@@ -64,7 +67,6 @@ redef class App
                app.on_start
                app.on_resume
 
-               native_window.show_all
                gtk_main
 
                app.on_pause
@@ -88,7 +90,13 @@ redef class App
                # improved with GTK 3.18 and interpolate_size.
                native_window.resizable = false
 
+               native_window.show_all
+
                super
+
+               if window.enable_back_button then
+                       back_button.native.show
+               else back_button.native.hide
        end
 end
 
@@ -243,6 +251,21 @@ redef class Button
        init do native.signal_connect("clicked", self, null)
 end
 
+# Button to go back between windows
+class BackButton
+       super Button
+
+       # TODO i18n
+       redef fun text=(value) do super(value or else "Back")
+
+       redef fun signal(sender, data)
+       do
+               super
+
+               app.window.on_back_button
+       end
+end
+
 redef class Label
        redef type NATIVE: GtkLabel
        redef var native = new GtkLabel("")
@@ -294,6 +317,9 @@ redef class CheckBox
        redef type NATIVE: GtkCheckButton
        redef var native = new GtkCheckButton
 
+       redef fun signal(sender, data) do notify_observers new ToggleEvent(self)
+       init do native.signal_connect("toggled", self, null)
+
        redef fun text do return native.text
        redef fun text=(value) do native.text = (value or else "").to_s
 
index 6bc8b36..2db66b8 100644 (file)
@@ -26,7 +26,8 @@ and the FFI enables calling services in different languages.
 A minimal example follows with a custom `Action` and using `FileServer`.
 
 More general examples are available at `lib/nitcorn/examples/`.
-It includes the configuration of `http://xymus.net/` which merges many other _nitcorn_ applications.
+For an example of a larger project merging many _nitcorn_ applications into one server,
+take a look at the configuration of `http://xymus.net/` at `../contrib/xymus_net/xymus_net.nit`.
 
 Larger projects using _nitcorn_ can be found in the `contrib/` folder:
 * _opportunity_ is a meetup planner heavily based on _nitcorn_.
index d1bdeae..b1bbed9 100644 (file)
@@ -2,10 +2,6 @@ all: bin/restful_annot
        mkdir -p bin/
        ../../../bin/nitc --dir bin src/nitcorn_hello_world.nit src/simple_file_server.nit
 
-xymus.net:
-       mkdir -p bin/
-       ../../../bin/nitc --dir bin/ src/xymus_net.nit
-
 pre-build: src/restful_annot_gen.nit
 src/restful_annot_gen.nit:
        ../../../bin/nitrestful -o $@ src/restful_annot.nit
index ea3212d..5995558 100644 (file)
@@ -71,6 +71,9 @@ class FileServer
        # Caching attributes of served files, used as the `cache-control` field in response headers
        var cache_control = "public, max-age=360" is writable
 
+       # Show directory listing?
+       var show_directory_listing = true is writable
+
        redef fun answer(request, turi)
        do
                var response
@@ -105,8 +108,8 @@ class FileServer
                                        end
                                end
 
-                               response = new HttpResponse(200)
-                               if local_file.file_stat.is_dir then
+                               var is_dir = local_file.file_stat.is_dir
+                               if show_directory_listing and is_dir then
                                        # Show the directory listing
                                        var title = turi
                                        var files = local_file.files
@@ -131,6 +134,7 @@ class FileServer
                                                header_code = header.write_to_string
                                        else header_code = ""
 
+                                       response = new HttpResponse(200)
                                        response.body = """
 <!DOCTYPE html>
 <head>
@@ -154,8 +158,9 @@ class FileServer
 </html>"""
 
                                        response.header["Content-Type"] = media_types["html"].as(not null)
-                               else
+                               else if not is_dir then
                                        # It's a single file
+                                       response = new HttpResponse(200)
                                        response.files.add local_file
 
                                        var ext = local_file.file_extension
@@ -168,8 +173,8 @@ class FileServer
 
                                        # Cache control
                                        response.header["cache-control"] = cache_control
-                               end
 
+                               else response = new HttpResponse(404)
                        else response = new HttpResponse(404)
                else response = new HttpResponse(403)
 
index 73c9626..37dd9db 100644 (file)
@@ -51,6 +51,7 @@ class MediaTypes
                types["jar"]        = "application/java-archive"
                types["war"]        = "application/java-archive"
                types["ear"]        = "application/java-archive"
+               types["json"]       = "application/json"
                types["hqx"]        = "application/mac-binhex40"
                types["pdf"]        = "application/pdf"
                types["cco"]        = "application/x-cocoa"
index 4d54aa9..005a4dd 100644 (file)
@@ -72,7 +72,7 @@ end
 
 redef class HttpRequest
        # The `Session` associated to this request
-       var session: nullable Session = null
+       var session: nullable Session = null is writable
 end
 
 redef class HttpRequestParser
index 07664f6..3ec5dc1 100644 (file)
@@ -226,6 +226,8 @@ private class UriParam
 
        # Parameters match everything.
        redef fun match(part) do return true
+
+       redef fun to_s do return name
 end
 
 # A static uri string like `users`.
@@ -237,6 +239,8 @@ private class UriString
 
        # Empty strings match everything otherwise matching is based on string equality.
        redef fun match(part) do return string.is_empty or string == part
+
+       redef fun to_s do return string
 end
 
 redef class Routes
index ba09bc1..cf4d0fa 100644 (file)
@@ -359,3 +359,72 @@ redef universal Int
                return self & 0x3FFF_FFFF
        end
 end
+
+redef universal Float
+       # Smoothened `self`, used by `ImprovedNoise`
+       private fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0)
+end
+
+# Direct translation of Ken Perlin's improved noise Java implementation
+#
+# This implementation differs from `PerlinNoise` on two main points.
+# This noise is calculated for a 3D point, vs 2D in `PerlinNoise`.
+# `PerlinNoise` is based off a customizable seed, while this noise has a static data source.
+class ImprovedNoise
+
+       # Permutations
+       private var p: Array[Int] = [151,160,137,91,90,15,
+               131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
+               190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
+               88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
+               77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
+               102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
+               135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
+               5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
+               223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
+               129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
+               251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
+               49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
+               138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180] * 2
+
+       # Noise value in [-1..1] at 3D coordinates `x, y, z`
+       fun noise(x, y, z: Float): Float
+       do
+               var xx = x.floor.to_i & 255
+               var yy = y.floor.to_i & 255
+               var zz = z.floor.to_i & 255
+
+               x -= x.floor
+               y -= y.floor
+               z -= z.floor
+
+               var u = x.fade
+               var v = y.fade
+               var w = z.fade
+
+               var a  = p[xx  ] + yy
+               var aa = p[a   ] + zz
+               var ab = p[a+1 ] + zz
+               var b  = p[xx+1] + yy
+               var ba = p[b   ] + zz
+               var bb = p[b+1 ] + zz
+
+               return w.lerp(v.lerp(u.lerp(grad(p[aa  ], x,     y,     z    ),
+                                           grad(p[ba  ], x-1.0, y,     z    )),
+                                    u.lerp(grad(p[ab  ], x,     y-1.0, z    ),
+                                           grad(p[bb  ], x-1.0, y-1.0, z    ))),
+                      v.lerp(u.lerp(grad(p[aa+1], x,     y,     z-1.0),
+                                           grad(p[ba+1], x-1.0, y,     z-1.0)),
+                                    u.lerp(grad(p[ab+1], x,     y-1.0, z-1.0),
+                                           grad(p[bb+1], x-1.0, y-1.0, z-1.0))))
+       end
+
+       # Value at a corner of the grid
+       private fun grad(hash: Int, x, y, z: Float): Float
+       do
+               var h = hash & 15
+               var u = if h < 8 then x else y
+               var v = if h < 4 then y else if h == 12 or h == 14 then x else z
+               return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v)
+       end
+end
diff --git a/lib/popcorn/.gitignore b/lib/popcorn/.gitignore
new file mode 100644 (file)
index 0000000..c32211a
--- /dev/null
@@ -0,0 +1 @@
+tests/out
diff --git a/lib/popcorn/Makefile b/lib/popcorn/Makefile
new file mode 100644 (file)
index 0000000..12db3df
--- /dev/null
@@ -0,0 +1,24 @@
+# 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.
+
+NITUNIT=../../bin/nitunit
+
+check:
+       $(NITUNIT) README.md
+       $(NITUNIT) pop_routes.nit
+       $(NITUNIT) pop_handlers.nit
+       $(NITUNIT) popcorn.nit
+       cd tests; make check
diff --git a/lib/popcorn/README.md b/lib/popcorn/README.md
new file mode 100644 (file)
index 0000000..08f7c9a
--- /dev/null
@@ -0,0 +1,815 @@
+# Popcorn
+
+**Why endure plain corn when you can pop it?!**
+
+Popcorn is a minimal yet powerful nit web application framework that provides cool
+features for lazy developpers.
+
+Popcorn is built over nitcorn to provide a clean and user friendly interface
+without all the boiler plate code.
+
+## What does it taste like?
+
+Set up is quick and easy as 10 lines of code.
+Create a file `app.nit` and add the following code:
+
+~~~
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
+~~~
+
+The Popcorn app listens on port 3000 for connections.
+The app responds with "Hello World!" for requests to the root URL (`/`) or **route**.
+For every other path, it will respond with a **404 Not Found**.
+
+The `req` (request) and `res` (response) parameters are the same that nitcorn provides
+so you can do anything else you would do in your route without Popcorn involved.
+
+Run the app with the following command:
+
+~~~bash
+$ nitc app.nit && ./app
+~~~
+
+Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
+
+Here the output using the `curl` command:
+
+~~~bash
+$ curl localhost:3000
+<h1>Hello World!</h1>
+
+$ curl localhost:3000/wrong_uri
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Not Found</title>
+</head>
+<body>
+<h1>404 Not Found</h1>
+</body>
+</html>
+~~~
+
+This is why we love popcorn!
+
+## Basic routing
+
+**Routing** refers to determining how an application responds to a client request
+to a particular endpoint, which is a URI (or path) and a specific HTTP request
+method GET, POST, PUT or DELETE (other methods are not suported yet).
+
+Each route can have one or more handler methods, which are executed when the route is matched.
+
+Route handlers definition takes the following form:
+
+~~~nitish
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun METHOD(req, res) do end
+end
+~~~
+
+Where:
+* `MyHandler` is the name of the handler you will add to the app.
+* `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
+
+The following example responds to GET and POST requests:
+
+~~~
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Got a GET request"
+       redef fun post(req, res) do res.send "Got a POST request"
+end
+~~~
+
+To make your handler responds to a specific route, you have to add it to the app.
+
+Respond to POST request on the root route (`/`), the application's home page:
+
+~~~
+var app = new App
+app.use("/", new MyHandler)
+~~~
+
+Respond to a request to the `/user` route:
+
+~~~
+app.use("/user", new MyHandler)
+~~~
+
+For more details about routing, see the routing section.
+
+## Serving static files with Popcorn
+
+To serve static files such as images, CSS files, and JavaScript files, use the
+Popcorn built-in handler `StaticHandler`.
+
+Pass the name of the directory that contains the static assets to the StaticHandler
+init method to start serving the files directly.
+For example, use the following code to serve images, CSS files, and JavaScript files
+in a directory named `public`:
+
+~~~
+app.use("/", new StaticHandler("public/"))
+~~~
+
+Now, you can load the files that are in the `public` directory:
+
+~~~raw
+http://localhost:3000/images/trollface.jpg
+http://localhost:3000/css/style.css
+http://localhost:3000/js/app.js
+http://localhost:3000/hello.html
+~~~
+
+Popcorn looks up the files relative to the static directory, so the name of the
+static directory is not part of the URL.
+To use multiple static assets directories, add the `StaticHandler` multiple times:
+
+~~~
+app.use("/", new StaticHandler("public/"))
+app.use("/", new StaticHandler("files/"))
+~~~
+
+Popcorn looks up the files in the order in which you set the static directories
+with the `use` method.
+
+To create a virtual path prefix (where the path does not actually exist in the file system)
+for files that are served by the `StaticHandler`, specify a mount path for the
+static directory, as shown below:
+
+~~~
+app.use("/static/", new StaticHandler("public/"))
+~~~
+
+Now, you can load the files that are in the public directory from the `/static`
+path prefix.
+
+~~~raw
+http://localhost:3000/static/images/trollface.jpg
+http://localhost:3000/static/css/style.css
+http://localhost:3000/static/js/app.js
+http://localhost:3000/static/hello.html
+~~~
+
+However, the path that you provide to the `StaticHandler` is relative to the
+directory from where you launch your app.
+If you run the app from another directory, it’s safer to use the absolute path of
+the directory that you want to serve.
+
+## Advanced Routing
+
+**Routing** refers to the definition of application end points (URIs) and how
+they respond to client requests. For an introduction to routing, see the Basic routing
+section.
+
+The following code is an example of a very basic route.
+
+~~~
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+~~~
+
+### Route methods
+
+A **route method** is derived from one of the HTTP methods, and is attached to an
+instance of the Handler class.
+
+The following code is an example of routes that are defined for the GET and the POST
+methods to the root of the app.
+
+~~~
+import popcorn
+
+class GetPostHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "GET request to the homepage"
+       redef fun post(req, res) do res.send "POST request to the homepage"
+end
+
+var app = new App
+app.use("/", new GetPostHandler)
+~~~
+
+Popcorn supports the following routing methods that correspond to HTTP methods:
+get, post, put, and delete.
+
+The request query string is accessed through the `req` parameter:
+
+~~~
+import popcorn
+import template
+
+class QueryStringHandler
+       super Handler
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Query string: {req.query_string}"
+               for name, arg in req.get_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+app.listen("localhost", 3000)
+~~~
+
+Post parameters can also be accessed through the `req` parameter:
+
+~~~
+import popcorn
+import template
+
+class PostHandler
+       super Handler
+
+       redef fun post(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Body: {req.body}"
+               for name, arg in req.post_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+app.listen("localhost", 3000)
+~~~
+
+There is a special routing method, `all(res, req)`, which is not derived from any
+HTTP method. This method is used to respond at a path for all request methods.
+
+In the following example, the handler will be executed for requests to "/user"
+whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
+
+~~~
+import popcorn
+
+class AllHandler
+       super Handler
+
+       redef fun all(req, res) do res.send "Every request to the homepage"
+end
+~~~
+
+Using the `all` method you can also implement other HTTP request methods.
+
+~~~
+import popcorn
+
+class MergeHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if req.method == "MERGE" then
+                       # handle that method
+               else super # keep handle GET, POST, PUT and DELETE methods
+       end
+end
+~~~
+
+### Route paths
+
+**Route paths**, in combination with a request handlers, define the endpoints at
+which requests can be made.
+Route paths can be strings, parameterized strings or glob patterns.
+Query strings such as `?q=foo`are not part of the route path.
+
+Popcorn uses the `Handler::match(uri)` method to match the route paths.
+
+Here are some examples of route paths based on strings.
+
+This route path will match requests to the root route, `/`.
+
+~~~
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Got a GET request"
+end
+
+var app = new App
+app.use("/", new MyHandler)
+~~~
+
+This route path will match requests to `/about`.
+
+~~~
+app.use("/about", new MyHandler)
+~~~
+
+This route path will match requests to `/random.text`.
+
+~~~
+app.use("/random.text", new MyHandler)
+~~~
+
+During the query/response process, routes are matched by order of declaration
+through the `App::use` method.
+
+The app declared in this example will try to match the routes in this order:
+
+1. `/`
+2. `/about`
+3. `/random.text`
+
+### Route parameters
+
+**Route parameters** are variable parts of a route path. They can be used to path
+arguments within the URI.\13
+Parameters in a route are prefixed with a colon `:` like in `:userId`, `:year`.
+
+The following example declares a handler `UserHome` that responds with the `user`
+name.
+
+~~~
+import popcorn
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               if user != null then
+                       res.send "Hello {user}"
+               else
+                       res.send("Nothing received", 400)
+               end
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+app.listen("localhost", 3000)
+~~~
+
+The `UserHome` handler listen to every path matching `/:user`. This can be `/Morriar`,
+`/10`, ... but not `/Morriar/profile` since route follow the strict matching rule.
+
+### Glob routes
+
+**Glob routes** are routes that match only on a prefix, thus accepting a wider range
+of URI.
+Glob routes end with the symbol `*`.
+
+Here we define a `UserItem` handler that will respond to any URI matching the prefix
+`/user/:user/item/:item`.
+Note that glob route are compatible with route parameters.
+
+~~~
+import popcorn
+
+class UserItem
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               var item = req.param("item")
+               if user == null or item == null then
+                       res.send("Nothing received", 400)
+               else
+                       res.send "Here the item {item} of the use {user}."
+               end
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+app.listen("localhost", 3000)
+~~~
+
+## Response methods
+
+The methods on the response object (`res`), can is used to manipulate the
+request-response cycle.
+If none of these methods are called from a route handler, the client request will
+receive a `404 Not found` error.
+
+* `res.html()` Send a HTML response.
+* `res.json()` Send a JSON response.
+* `res.redirect()` Redirect a request.
+* `res.send()` Send a response of various types.
+* `res.error()` Set the response status code and send its message as the response body.
+
+## Middlewares
+
+### Overview
+
+**Middleware** handlers are handlers that typically do not send `HttpResponse` responses.
+Middleware handlers can perform the following tasks:
+
+* Execute any code.
+* Make changes to the request and the response objects.
+* End its action and pass to the next handler in the stack.
+
+If a middleware handler makes a call to `res.send()`, it provoques the end the
+request-response cycle and the response is sent to the client.
+
+### Ultra simple logger example
+
+Here is an example of a simple “Hello World” Popcorn application.
+We add a middleware handler to the application called MyLogger that prints a simple
+log message in the app stdout.
+
+~~~
+import popcorn
+
+class MyLogger
+       super Handler
+
+       redef fun all(req, res) do print "Request Logged!"
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+
+var app = new App
+app.use("/*", new MyLogger)
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
+~~~
+
+By using the `MyLogger` handler to the route `/*` we ensure that every requests
+(even 404 ones) pass through the middleware handler.
+This handler just prints “Request Logged!” when a request is received.
+
+The order of middleware loading is important: middleware functions that are loaded first are also executed first.
+In the above example, `MyLogger` will be executed before `HelloHandler`.
+
+### Ultra cool, more advanced logger example
+
+Next, we’ll create a middleware handler called “LogHandler” that prints the requested
+uri, the response status and the time it took to Popcorn to process the request.
+
+This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares.
+
+~~~
+import popcorn
+import realtime
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var timer: nullable Clock = null
+end
+
+class RequestTimeHandler
+       super Handler
+
+       redef fun all(req, res) do req.timer = new Clock
+end
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do
+               var timer = req.timer
+               if timer != null then
+                       print "{req.method} {req.uri} {res.color_status} ({timer.total})"
+               else
+                       print "{req.method} {req.uri} {res.color_status}"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use("/*", new LogHandler)
+app.listen("localhost", 3000)
+~~~
+
+First, we attach a new attribute `timer` to every `HttpRequest`.
+Doing so we can access our data from all handlers that import our module, directly
+from the `req` parameter.
+
+We use the new middleware called `RequestTimeHandler` to initialize the request timer.
+
+Finally, our `LogHandler` will display a bunch of data and use the request `timer`
+to display the time it took to process the request.
+
+The app now uses the `RequestTimeHandler` middleware for every requests received
+by the Popcorn app.
+The page is processed the `HelloHandler` to display the index page.
+And, before every response is sent, the `LogHandler` is activated to display the
+log line.
+
+Because you have access to the request object, the response object, and all the
+Popcorn API, the possibilities with middleware functions are endless.
+
+### Built-in middlewares
+
+Starting with version 0.1, Popcorn provide a set of built-in middleware that can
+be used to develop your app faster.
+
+* `RequestClock`: initializes requests clock.
+* `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`).
+* `SessionInit`: initializes requests session (see the `Sessions` section).
+* `StaticServer`: serves static files (see the `Serving static files with Popcorn` section).
+* `Router`: a mountable mini-app (see the `Mountable routers` section).
+
+## Mountable routers
+
+Use the `Router` class to create modular, mountable route handlers.
+A Router instance is a complete middleware and routing system; for this reason,
+it is often referred to as a “mini-app”.
+
+The following example creates a router as a module, loads a middleware handler in it,
+defines some routes, and mounts the router module on a path in the main app.
+
+~~~
+import popcorn
+
+class AppHome
+       super Handler
+
+       redef fun get(req, res) do res.send "Site Home"
+end
+
+class UserLogger
+       super Handler
+
+       redef fun all(req, res) do print "User logged"
+end
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do res.send "User Home"
+end
+
+class UserProfile
+       super Handler
+
+       redef fun get(req, res) do res.send "User Profile"
+end
+
+var user_router = new Router
+user_router.use("/*", new UserLogger)
+user_router.use("/", new UserHome)
+user_router.use("/profile", new UserProfile)
+
+var app = new App
+app.use("/", new AppHome)
+app.use("/user", user_router)
+app.listen("localhost", 3000)
+~~~
+
+The app will now be able to handle requests to /user and /user/profile, as well
+as call the `Time` middleware handler that is specific to the route.
+
+## Error handling
+
+**Error handling** is based on middleware handlers.
+
+Define error-handling middlewares in the same way as other middleware handlers:
+
+~~~
+import popcorn
+
+class SimpleErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       print "An error occurred! {res.status_code})"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+app.listen("localhost", 3000)
+~~~
+
+In this example, every non-200 response is caught by the `SimpleErrorHandler`
+that print an error in stdout.
+
+By defining multiple middleware error handlers, you can take multiple action depending
+on the kind of error or the kind of interface you provide (HTML, XML, JSON...).
+
+Here an example of the 404 custom error page in HTML:
+
+~~~
+import popcorn
+import template
+
+class HtmlErrorTemplate
+       super Template
+
+       var status: Int
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+end
+
+class HtmlErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!"))
+               end
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+app.listen("localhost", 3000)
+~~~
+
+## Sessions
+
+**Sessions** can be used thanks to the built-in `SessionMiddleware`.
+
+Here a simple example of login button that define a value in the `req` session.
+
+~~~
+import popcorn
+
+redef class Session
+       var is_logged = false
+end
+
+class AppLogin
+       super Handler
+
+       redef fun get(req, res) do
+               res.html """
+               <p>Is logged: {{{req.session.as(not null).is_logged}}}</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>"""
+       end
+
+       redef fun post(req, res) do
+               req.session.as(not null).is_logged = true
+               res.redirect("/")
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+app.listen("localhost", 3000)
+~~~
+
+Notice the use of the `SessionInit` on the `/*` route. You must use the
+`SessionInit` first to initialize the request session.
+Without that, your request session will be set to `null`.
+If you don't use sessions in your app, you do not need to include that middleware.
+
+## Database integration
+
+### Mongo DB
+
+If you want to persist your data, Popcorn works well with MongoDB.
+
+In this example, we will show how to store and list user with a Mongo database.
+
+First let's define a handler that access the database to list all the user.
+The mongo database reference is passed to the UserList handler through the `db` attribute.
+
+Then we define a handler that displays the user creation form on GET requests.
+POST requests are used to save the user data.
+
+~~~
+import popcorn
+import mongodb
+import template
+
+class UserList
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var users = db.collection("users").find_all(new JsonObject)
+
+               var tpl = new Template
+               tpl.add "<h1>Users</h1>"
+               tpl.add "<table>"
+               for user in users do
+                       tpl.add """<tr>
+                               <td>{{{user["login"] or else "null"}}}</td>
+                               <td>{{{user["password"] or else "null"}}}</td>
+                       </tr>"""
+               end
+               tpl.add "</table>"
+               res.html tpl
+       end
+end
+
+class UserForm
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.add """<h1>Add a new user</h1>
+               <form action="/new" method="POST">
+                       <input type="text" name="login" />
+                       <input type="password" name="password" />
+                       <input type="submit" value="save" />
+               </form>"""
+               res.html tpl
+       end
+
+       redef fun post(req, res) do
+               var json = new JsonObject
+               json["login"] = req.post_args["login"]
+               json["password"] = req.post_args["password"]
+               db.collection("users").insert(json)
+               res.redirect "/"
+       end
+end
+
+var mongo = new MongoClient("mongodb://localhost:27017/")
+var db = mongo.database("mongo_example")
+
+var app = new App
+app.use("/", new UserList(db))
+app.use("/new", new UserForm(db))
+app.listen("localhost", 3000)
+~~~
+
+## Angular.JS integration
+
+Loving [AngularJS](https://angularjs.org/)? Popcorn is made for Angular and for you!
+
+Using the StaticHandler with a glob route, you can easily redirect all HTTP requests
+to your angular controller:
+
+~~~
+import popcorn
+
+var app = new App
+app.use("/*", new StaticHandler("my-ng-app/"))
+app.listen("localhost", 3000)
+~~~
+
+See the examples for a more detailed use case working with a JSON API.
diff --git a/lib/popcorn/examples/angular/example_angular.nit b/lib/popcorn/examples/angular/example_angular.nit
new file mode 100644 (file)
index 0000000..ba7f4f2
--- /dev/null
@@ -0,0 +1,44 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 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.
+
+import popcorn
+
+class CounterAPI
+       super Handler
+
+       var counter = 0
+
+       fun json_counter: JsonObject do
+               var json = new JsonObject
+               json["label"] = "Visitors"
+               json["value"] = counter
+               return json
+       end
+
+       redef fun get(req, res) do
+               res.json(json_counter)
+       end
+
+       redef fun post(req, res) do
+               counter += 1
+               res.json(json_counter)
+       end
+end
+
+var app = new App
+app.use("/counter", new CounterAPI)
+app.use("/*", new StaticHandler("www/"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/angular/www/index.html b/lib/popcorn/examples/angular/www/index.html
new file mode 100644 (file)
index 0000000..59bc4d1
--- /dev/null
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang='en' ng-app='ng-example'>
+       <head>
+               <base href='/'>
+               <title>ng-example</title>
+       </head>
+       <body>
+               <div ng-view></div>
+
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.min.js'></script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.js'></script>
+               <script src='/javascripts/ng-example.js'></script>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/angular/www/javascripts/ng-example.js b/lib/popcorn/examples/angular/www/javascripts/ng-example.js
new file mode 100644 (file)
index 0000000..f9270d2
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+(function() {
+       angular
+
+       .module('ng-example', ['ngRoute'])
+
+       .factory('CounterModel', ['$http', function($http) {
+               return {
+                       load: function(cb, cbErr) {
+                               $http.get('/counter')
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       increment: function(cb, cbErr) {
+                               $http.post('/counter')
+                                       .success(cb)
+                                       .error(cbErr);
+                       }
+               };
+       }])
+
+       .controller('CounterCtrl', ['CounterModel', function(CounterModel) {
+               var $this = this;
+
+               this.loadCounter = function() {
+                       CounterModel.load(
+                               function(data) {
+                                       $this.counter = data;
+                               }, function(err) {
+                                       $this.error = err;
+                               });
+               };
+
+               this.incrementCounter = function() {
+                       CounterModel.increment(
+                               function(data) {
+                                       $this.counter = data;
+                               }, function(err) {
+                                       $this.error = err;
+                               });
+               };
+
+               this.loadCounter();
+       }])
+
+       .config(function($routeProvider, $locationProvider) {
+               $routeProvider
+                       .when('/', {
+                               templateUrl: 'views/index.html',
+                               controller: 'CounterCtrl',
+                               controllerAs: 'counterCtrl'
+                       })
+                       .otherwise({
+                               redirectTo: '/'
+                       });
+               $locationProvider.html5Mode(true);
+       });
+})();
diff --git a/lib/popcorn/examples/angular/www/views/index.html b/lib/popcorn/examples/angular/www/views/index.html
new file mode 100644 (file)
index 0000000..058e799
--- /dev/null
@@ -0,0 +1,9 @@
+<h1>Nit &hearts; Angular.JS</h1>
+
+<p>Click the button to increment the counter.</p>
+
+<form>
+       <label>{{counterCtrl.counter.label}}</label>
+       <input type="number" ng-model="counterCtrl.counter.value" readonly />
+       <button ng-click="counterCtrl.incrementCounter()">Increment!</button>
+</form>
diff --git a/lib/popcorn/examples/handlers/example_post_handler.nit b/lib/popcorn/examples/handlers/example_post_handler.nit
new file mode 100644 (file)
index 0000000..826c165
--- /dev/null
@@ -0,0 +1,36 @@
+# 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.
+
+import popcorn
+import template
+
+class PostHandler
+       super Handler
+
+       redef fun post(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Body: {req.body}"
+               for name, arg in req.post_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/handlers/example_query_string.nit b/lib/popcorn/examples/handlers/example_query_string.nit
new file mode 100644 (file)
index 0000000..e8c6187
--- /dev/null
@@ -0,0 +1,36 @@
+# 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.
+
+import popcorn
+import template
+
+class QueryStringHandler
+       super Handler
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Query string: {req.query_string}"
+               for name, arg in req.get_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/hello_world/example_hello.nit b/lib/popcorn/examples/hello_world/example_hello.nit
new file mode 100644 (file)
index 0000000..fc9f3e5
--- /dev/null
@@ -0,0 +1,27 @@
+# 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.
+
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_advanced_logger.nit b/lib/popcorn/examples/middlewares/example_advanced_logger.nit
new file mode 100644 (file)
index 0000000..b97d10c
--- /dev/null
@@ -0,0 +1,54 @@
+# 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.
+
+import popcorn
+import realtime
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var timer: nullable Clock = null
+end
+
+class RequestTimeHandler
+       super Handler
+
+       redef fun all(req, res) do req.timer = new Clock
+end
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do
+               var timer = req.timer
+               if timer != null then
+                       print "{req.method} {req.uri} {res.color_status} ({timer.total})"
+               else
+                       print "{req.method} {req.uri} {res.color_status}"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use("/*", new LogHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_html_error_handler.nit b/lib/popcorn/examples/middlewares/example_html_error_handler.nit
new file mode 100644 (file)
index 0000000..5bd399c
--- /dev/null
@@ -0,0 +1,51 @@
+# 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.
+
+import popcorn
+import template
+
+class HtmlErrorTemplate
+       super Template
+
+       var status: Int
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+end
+
+class HtmlErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!"))
+               end
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_simple_error_handler.nit b/lib/popcorn/examples/middlewares/example_simple_error_handler.nit
new file mode 100644 (file)
index 0000000..16a5977
--- /dev/null
@@ -0,0 +1,39 @@
+# 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.
+
+import popcorn
+
+class SimpleErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send("An error occurred!", res.status_code)
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_simple_logger.nit b/lib/popcorn/examples/middlewares/example_simple_logger.nit
new file mode 100644 (file)
index 0000000..98be552
--- /dev/null
@@ -0,0 +1,35 @@
+# 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.
+
+import popcorn
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do print "Request Logged"
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+
+var app = new App
+app.use("/*", new LogHandler)
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/mongodb/example_mongodb.nit b/lib/popcorn/examples/mongodb/example_mongodb.nit
new file mode 100644 (file)
index 0000000..4f89f5b
--- /dev/null
@@ -0,0 +1,66 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 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.
+
+import popcorn
+import mongodb
+import template
+
+class UserList
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var users = db.collection("users").find_all(new JsonObject)
+
+               var tpl = new Template
+               tpl.add """
+               <h1>Users</h1>
+
+               <h2>Add a new user</h2>
+               <form action="/" method="POST">
+                       <input type="text" name="login" />
+                       <input type="password" name="password" />
+                       <input type="submit" value="save" />
+               </form>
+
+               <h2>All users</h2>
+               <table>"""
+               for user in users do
+                       tpl.add """<tr>
+                       <td>{{{user["login"] or else "null"}}}</td>
+                       <td>{{{user["password"] or else "null"}}}</td>
+                       </tr>"""
+               end
+               tpl.add "</table>"
+               res.html(tpl)
+       end
+
+       redef fun post(req, res) do
+               var json = new JsonObject
+               json["login"] = req.post_args["login"]
+               json["password"] = req.post_args["password"]
+               db.collection("users").insert(json)
+               res.redirect("/")
+       end
+end
+
+var mongo = new MongoClient("mongodb://localhost:27017/")
+var db = mongo.database("mongo_example")
+
+var app = new App
+app.use("/", new UserList(db))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/routing/example_glob_route.nit b/lib/popcorn/examples/routing/example_glob_route.nit
new file mode 100644 (file)
index 0000000..b33caec
--- /dev/null
@@ -0,0 +1,35 @@
+# 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.
+
+import popcorn
+
+class UserItem
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               var item = req.param("item")
+               if user == null or item == null then
+                       res.send("Nothing received", 400)
+               else
+                       res.send "Here the item {item} of the use {user}."
+               end
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/routing/example_param_route.nit b/lib/popcorn/examples/routing/example_param_route.nit
new file mode 100644 (file)
index 0000000..f536771
--- /dev/null
@@ -0,0 +1,34 @@
+# 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.
+
+import popcorn
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               if user != null then
+                       res.send "Hello {user}"
+               else
+                       res.send("Nothing received", 400)
+               end
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/routing/example_router.nit b/lib/popcorn/examples/routing/example_router.nit
new file mode 100644 (file)
index 0000000..91aa3ab
--- /dev/null
@@ -0,0 +1,51 @@
+# 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.
+
+import popcorn
+
+class AppHome
+       super Handler
+
+       redef fun get(req, res) do res.send "Site Home"
+end
+
+class UserLogger
+       super Handler
+
+       redef fun all(req, res) do print "User logged"
+end
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do res.send "User Home"
+end
+
+class UserProfile
+       super Handler
+
+       redef fun get(req, res) do res.send "User Profile"
+end
+
+var user_router = new Router
+user_router.use("/*", new UserLogger)
+user_router.use("/", new UserHome)
+user_router.use("/profile", new UserProfile)
+
+var app = new App
+app.use("/", new AppHome)
+app.use("/user", user_router)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/sessions/example_session.nit b/lib/popcorn/examples/sessions/example_session.nit
new file mode 100644 (file)
index 0000000..be79fb1
--- /dev/null
@@ -0,0 +1,43 @@
+# 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.
+
+import popcorn
+
+redef class Session
+       var is_logged = false
+end
+
+class AppLogin
+       super Handler
+
+       redef fun get(req, res) do
+               res.html """
+               <p>Is logged: {{{req.session.as(not null).is_logged}}}</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>"""
+       end
+
+       redef fun post(req, res) do
+               req.session.as(not null).is_logged = true
+               res.redirect("/")
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/example_static.nit b/lib/popcorn/examples/static_files/example_static.nit
new file mode 100644 (file)
index 0000000..c3da6d0
--- /dev/null
@@ -0,0 +1,21 @@
+# 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.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/example_static_multiple.nit b/lib/popcorn/examples/static_files/example_static_multiple.nit
new file mode 100644 (file)
index 0000000..1c730c6
--- /dev/null
@@ -0,0 +1,24 @@
+# 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.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/"))
+app.use("/", new StaticHandler("files/"))
+app.use("/static", new StaticHandler("public/"))
+app.use("/static", new StaticHandler("files/"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/files/index.html b/lib/popcorn/examples/static_files/files/index.html
new file mode 100644 (file)
index 0000000..bb850bd
--- /dev/null
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/static_files/public/css/style.css b/lib/popcorn/examples/static_files/public/css/style.css
new file mode 100644 (file)
index 0000000..5ba0171
--- /dev/null
@@ -0,0 +1,4 @@
+body {
+       color: blue;
+       padding: 20px;
+}
diff --git a/lib/popcorn/examples/static_files/public/hello.html b/lib/popcorn/examples/static_files/public/hello.html
new file mode 100644 (file)
index 0000000..61521ac
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/static_files/public/images/trollface.jpg b/lib/popcorn/examples/static_files/public/images/trollface.jpg
new file mode 100644 (file)
index 0000000..0c733bc
Binary files /dev/null and b/lib/popcorn/examples/static_files/public/images/trollface.jpg differ
diff --git a/lib/popcorn/examples/static_files/public/js/app.js b/lib/popcorn/examples/static_files/public/js/app.js
new file mode 100644 (file)
index 0000000..4a1510d
--- /dev/null
@@ -0,0 +1 @@
+alert("Hello World!");
diff --git a/lib/popcorn/package.ini b/lib/popcorn/package.ini
new file mode 100644 (file)
index 0000000..8f99b9c
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name=popcorn
+tags=web,lib
+maintainer=Alexandre Terrasa <alexandre@moz-code.org>
+license=Apache-2.0
+version=1.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/popcorn/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/popcorn/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/lib/popcorn/pop_handlers.nit b/lib/popcorn/pop_handlers.nit
new file mode 100644 (file)
index 0000000..2c85e15
--- /dev/null
@@ -0,0 +1,428 @@
+# 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.
+
+# Route handlers.
+module pop_handlers
+
+import pop_routes
+import json
+
+# Class handler for a route.
+#
+# **Routing** refers to determining how an application responds to a client request
+# to a particular endpoint, which is a URI (or path) and a specific HTTP request
+# method GET, POST, PUT or DELETE (other methods are not suported yet).
+#
+# Each route can have one or more handler methods, which are executed when the route is matched.
+#
+# Route handlers definition takes the following form:
+#
+# ~~~nitish
+# class MyHandler
+#      super Handler
+#
+#      redef fun METHOD(req, res) do end
+# end
+# ~~~
+#
+# Where:
+# * `MyHandler` is the name of the handler you will add to the app.
+# * `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
+#
+# The following example responds with `Hello World!` to GET and POST requests:
+#
+# ~~~
+# class MyHandler
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "Got a GET request"
+#      redef fun post(req, res) do res.send "Got a POST request"
+# end
+# ~~~
+#
+# To make your handler responds to a specific route, you have to add it to the app.
+#
+# Respond to POST request on the root route (`/`), the application's home page:
+#
+# ~~~
+# var app = new App
+# app.use("/", new MyHandler)
+# ~~~
+#
+# Respond to a request to the `/user` route:
+#
+# ~~~
+# app.use("/user", new MyHandler)
+# ~~~
+abstract class Handler
+
+       # Call `all(req, res)` if `route` matches `uri`.
+       private fun handle(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               if route.match(uri) then
+                       if route isa AppParamRoute then
+                               req.uri_params = route.parse_uri_parameters(uri)
+                       end
+                       all(req, res)
+               end
+       end
+
+       # Handler to all kind of HTTP request methods.
+       #
+       # `all` is a special request handler, which is not derived from any
+       # HTTP method. This method is used to respond at a path for all request methods.
+       #
+       # In the following example, the handler will be executed for requests to "/user"
+       # whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
+       #
+       # ~~~
+       # class AllHandler
+       #       super Handler
+       #
+       #       redef fun all(req, res) do res.send "Every request to the homepage"
+       # end
+       # ~~~
+       #
+       # Using the `all` method you can also implement other HTTP request methods.
+       #
+       # ~~~
+       # class MergeHandler
+       #       super Handler
+       #
+       #       redef fun all(req, res) do
+       #               if req.method == "MERGE" then
+       #                       # handle that method
+       #               else super # keep handle GET, POST, PUT and DELETE methods
+       #       end
+       # end
+       # ~~~
+       fun all(req: HttpRequest, res: HttpResponse) do
+               if req.method == "GET" then
+                       get(req, res)
+               else if req.method == "POST" then
+                       post(req, res)
+               else if req.method == "PUT" then
+                       put(req, res)
+               else if req.method == "DELETE" then
+                       delete(req, res)
+               else
+                       res.status_code = 405
+               end
+       end
+
+       # GET handler.
+       #
+       # Exemple of route responding to GET requests.
+       # ~~~
+       # class GetHandler
+       #       super Handler
+       #
+       #       redef fun get(req, res) do res.send "GETrequest received"
+       # end
+       # ~~~
+       fun get(req: HttpRequest, res: HttpResponse) do end
+
+       # POST handler.
+       #
+       # Exemple of route responding to POST requests.
+       # ~~~
+       # class PostHandler
+       #       super Handler
+       #
+       #       redef fun post(req, res) do res.send "POST request received"
+       # end
+       # ~~~
+       fun post(req: HttpRequest, res: HttpResponse) do end
+
+       # PUT handler.
+       #
+       # Exemple of route responding to PUT requests.
+       # ~~~
+       # class PutHandler
+       #       super Handler
+       #
+       #       redef fun put(req, res) do res.send "PUT request received"
+       # end
+       # ~~~
+       fun put(req: HttpRequest, res: HttpResponse) do end
+
+       # DELETE handler.
+       #
+       # Exemple of route responding to PUT requests.
+       # ~~~
+       # class DeleteHandler
+       #       super Handler
+       #
+       #       redef fun delete(req, res) do res.send "DELETE request received"
+       # end
+       # ~~~
+       fun delete(req: HttpRequest, res: HttpResponse) do end
+end
+
+# Static files server.
+#
+# To serve static files such as images, CSS files, and JavaScript files, use the
+# Popcorn built-in handler `StaticHandler`.
+#
+# Pass the name of the directory that contains the static assets to the StaticHandler
+# init method to start serving the files directly.
+# For example, use the following code to serve images, CSS files, and JavaScript files
+# in a directory named `public`:
+#
+# ~~~
+# var app = new App
+# app.use("/", new StaticHandler("public/"))
+# ~~~
+#
+# Now, you can load the files that are in the `public` directory:
+#
+# ~~~raw
+# http://localhost:3000/images/trollface.jpg
+# http://localhost:3000/css/style.css
+# http://localhost:3000/js/app.js
+# http://localhost:3000/hello.html
+# ~~~
+#
+# Popcorn looks up the files relative to the static directory, so the name of the
+# static directory is not part of the URL.
+# To use multiple static assets directories, add the `StaticHandler` multiple times:
+#
+# ~~~
+# app.use("/", new StaticHandler("public/"))
+# app.use("/", new StaticHandler("files/"))
+# ~~~
+#
+# Popcorn looks up the files in the order in which you set the static directories
+# with the `use` method.
+#
+# To create a virtual path prefix (where the path does not actually exist in the file system)
+# for files that are served by the `StaticHandler`, specify a mount path for the
+# static directory, as shown below:
+#
+# ~~~
+# app.use("/static/", new StaticHandler("public/"))
+# ~~~
+#
+# Now, you can load the files that are in the public directory from the `/static`
+# path prefix.
+#
+# ~~~raw
+# http://localhost:3000/static/images/trollface.jpg
+# http://localhost:3000/static/css/style.css
+# http://localhost:3000/static/js/app.js
+# http://localhost:3000/static/hello.html
+# ~~~
+#
+# However, the path that you provide to the `StaticHandler` is relative to the
+# directory from where you launch your app.
+# If you run the app from another directory, it’s safer to use the absolute path of
+# the directory that you want to serve.
+class StaticHandler
+       super Handler
+
+       # Static files directory to serve.
+       var static_dir: String
+
+       # Internal file server used to lookup and render files.
+       var file_server: FileServer is lazy do
+               var srv = new FileServer(static_dir)
+               srv.show_directory_listing = false
+               return srv
+       end
+
+       redef fun handle(route, uri, req, res) do
+               var answer = file_server.answer(req, route.uri_root(uri))
+               if answer.status_code == 200 then
+                       res.status_code = answer.status_code
+                       res.header.add_all answer.header
+                       res.files.add_all answer.files
+                       res.send
+               else if answer.status_code != 404 then
+                       res.status_code = answer.status_code
+               end
+       end
+end
+
+# Mountable routers
+#
+# Use the `Router` class to create modular, mountable route handlers.
+# A Router instance is a complete middleware and routing system; for this reason,
+# it is often referred to as a “mini-app”.
+#
+# The following example creates a router as a module, loads a middleware handler in it,
+# defines some routes, and mounts the router module on a path in the main app.
+#
+# ~~~
+# class AppHome
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "Site Home"
+# end
+#
+# class UserLogger
+#      super Handler
+#
+#      redef fun all(req, res) do print "User logged"
+# end
+#
+# class UserHome
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "User Home"
+# end
+#
+# class UserProfile
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "User Profile"
+# end
+#
+# var user_router = new Router
+# user_router.use("/*", new UserLogger)
+# user_router.use("/", new UserHome)
+# user_router.use("/profile", new UserProfile)
+#
+# var app = new App
+# app.use("/", new AppHome)
+# app.use("/user", user_router)
+# ~~~
+#
+# The app will now be able to handle requests to /user and /user/profile, as well
+# as call the `Time` middleware handler that is specific to the route.
+class Router
+       super Handler
+
+       # List of handlers to match with requests.
+       private var handlers = new Map[AppRoute, Handler]
+
+       # Register a `handler` for a route `path`.
+       #
+       # Route paths are matched in registration order.
+       fun use(path: String, handler: Handler) do
+               var route
+               if handler isa Router or handler isa StaticHandler then
+                       route = new AppGlobRoute(path)
+               else if path.has_suffix("*") then
+                       route = new AppGlobRoute(path)
+               else
+                       route = new AppParamRoute(path)
+               end
+               handlers[route] = handler
+       end
+
+       redef fun handle(route, uri, req, res) do
+               if not route.match(uri) then return
+               for hroute, handler in handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+                       if res.sent then break
+               end
+       end
+end
+
+# Popcorn application.
+#
+# The `App`  is the main point of the application.
+# It acts as a `Router` that holds the top level route handlers.
+#
+# Here an example to create a simple web app with Popcorn:
+#
+# ~~~
+# import popcorn
+#
+# class HelloHandler
+#      super Handler
+#
+#      redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+# end
+#
+# var app = new App
+# app.use("/", new HelloHandler)
+# # app.listen("localhost", 3000)
+# ~~~
+#
+# The Popcorn app listens on port 3000 for connections.
+# The app responds with "Hello World!" for request to the root URL (`/`) or **route**.
+# For every other path, it will respond with a **404 Not Found**.
+#
+# The `req` (request) and `res` (response) parameters are the same that nitcorn provides
+# so you can do anything else you would do in your route without Popcorn involved.
+#
+# Run the app with the following command:
+#
+# ~~~bash
+# nitc app.nit && ./app
+# ~~~
+#
+# Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
+class App
+       super Router
+end
+
+redef class HttpResponse
+
+       # Was this request sent by a handler?
+       var sent = false
+
+       private fun check_sent do
+               if sent then print "Warning: Headers already sent!"
+       end
+
+       # Write data in body response and send it.
+       fun send(raw_data: nullable Writable, status: nullable Int) do
+               if raw_data != null then
+                       body += raw_data.write_to_string
+               end
+               if status != null then
+                       status_code = status
+               else
+                       status_code = 200
+               end
+               check_sent
+               sent = true
+       end
+
+       # Write data as HTML and set the right content type header.
+       fun html(html: nullable Writable, status: nullable Int) do
+               header["Content-Type"] = media_types["html"].as(not null)
+               send(html, status)
+       end
+
+       # Write data as JSON and set the right content type header.
+       fun json(json: nullable Jsonable, status: nullable Int) do
+               header["Content-Type"] = media_types["json"].as(not null)
+               if json == null then
+                       send(null, status)
+               else
+                       send(json.to_json, status)
+               end
+       end
+
+       # Redirect response to `location`
+       fun redirect(location: String, status: nullable Int) do
+               header["Location"] = location
+               if status != null then
+                       status_code = status
+               else
+                       status_code = 302
+               end
+               check_sent
+               sent = true
+       end
+
+       # TODO The error message should be parameterizable.
+       fun error(status: Int) do
+               html("Error", status)
+       end
+end
diff --git a/lib/popcorn/pop_middlewares.nit b/lib/popcorn/pop_middlewares.nit
new file mode 100644 (file)
index 0000000..38e4bd9
--- /dev/null
@@ -0,0 +1,77 @@
+# 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.
+
+module pop_middlewares
+
+import pop_handlers
+import console
+import realtime
+
+# Initialize session in request if non existent.
+#
+# Should be called before any use of the session.
+class SessionInit
+       super Handler
+
+       redef fun all(req, res) do if req.session == null then req.session = new Session
+end
+
+# Initialize a clock for the resquest.
+#
+# Can be used to compute the time passed to respond that request.
+class RequestClock
+       super Handler
+
+       redef fun all(req, res) do req.clock = new Clock
+end
+
+# Display log info about request processing.
+class ConsoleLog
+       super Handler
+
+       # Do we want colors in the console output?
+       var colors = true
+
+       redef fun all(req, res) do
+               var clock = req.clock
+               if clock != null then
+                       print "{req.method} {req.uri} {status(res)} ({clock.total})"
+               else
+                       print "{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
+       end
+end
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var clock: nullable Clock = null
+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
+       end
+end
diff --git a/lib/popcorn/pop_routes.nit b/lib/popcorn/pop_routes.nit
new file mode 100644 (file)
index 0000000..68c3d37
--- /dev/null
@@ -0,0 +1,263 @@
+# 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.
+
+# Internal routes representation.
+module pop_routes
+
+import nitcorn
+
+# AppRoute provide services for path and uri manipulation and matching..
+#
+# Default strict routes like `/` or `/user` match the same URI string.
+# An exception is done for the trailing `/`, which is always omitted during the
+# parsing.
+#
+# ~~~
+# var route = new AppRoute("/")
+# assert route.match("")
+# assert route.match("/")
+# assert not route.match("/user")
+# assert not route.match("user")
+#
+# route = new AppRoute("/user")
+# assert not route.match("/")
+# assert route.match("/user")
+# assert route.match("/user/")
+# assert not route.match("/user/10")
+# assert not route.match("/foo")
+# assert not route.match("user")
+# assert not route.match("/username")
+# ~~~
+class AppRoute
+
+       # Route relative path from server root.
+       var path: String
+
+       # Does self match the `req`?
+       fun match(uri: String): Bool do
+               uri = uri.simplify_path
+               var path = resolve_path(uri)
+               if uri.is_empty and path == "/" then return true
+               return uri == path
+       end
+
+       # Replace path parameters with concrete values from the `uri`.
+       #
+       # For strict routes, it returns the path unchanged:
+       # ~~~
+       # var route = new AppRoute("/")
+       # assert route.resolve_path("/user/10/profile") == "/"
+       #
+       # route = new AppRoute("/user")
+       # assert route.resolve_path("/user/10/profile") == "/user"
+       # ~~~
+       fun resolve_path(uri: String): String do return path.simplify_path
+
+       # Remove `resolved_path` prefix from `uri`.
+       #
+       # Mainly used to resolve and match mountable routes.
+       #
+       # ~~~
+       # var route = new AppRoute("/")
+       # assert route.uri_root("/user/10/profile") == "/user/10/profile"
+       #
+       # route = new AppRoute("/user")
+       # assert route.uri_root("/user/10/profile") == "/10/profile"
+       # ~~~
+       fun uri_root(uri: String): String do
+               var path = resolve_path(uri)
+               if path == "/" then return uri
+               return uri.substring(path.length, uri.length).simplify_path
+       end
+end
+
+# Parameterizable routes.
+#
+# Routes that can contains variables parts that will be resolved during the
+# matching process.
+#
+# Route parameters are marked with a colon `:`
+# ~~~
+# var route = new AppParamRoute("/:id")
+# assert not route.match("/")
+# assert route.match("/user")
+# assert route.match("/user/")
+# assert not route.match("/user/10")
+# ~~~
+#
+# It is possible to use more than one parameter in the same route:
+# ~~~
+# route = new AppParamRoute("/user/:userId/items/:itemId")
+# assert not route.match("/user/10/items/")
+# assert route.match("/user/10/items/e895346")
+# assert route.match("/user/USER/items/0/")
+# assert not route.match("/user/10/items/10/profile")
+# ~~~
+class AppParamRoute
+       super AppRoute
+
+       init do parse_path_parameters(path)
+
+       # Cut `path` into `UriParts`.
+       fun parse_path_parameters(path: String) do
+               for part in path.split("/") do
+                       if not part.is_empty and part.first == ':' then
+                               # is an uri param
+                               path_parts.add new UriParam(part.substring(1, part.length))
+                       else
+                               # is a standard string
+                               path_parts.add new UriString(part)
+                       end
+               end
+       end
+
+       # For parameterized routes, parameter names are replaced by their value in the URI.
+       # ~~~
+       # var route = new AppParamRoute("/user/:id")
+       # assert route.resolve_path("/user/10/profile") == "/user/10"
+       #
+       # route = new AppParamRoute("/user/:userId/items/:itemId")
+       # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
+       # ~~~
+       redef fun resolve_path(uri) do
+               var uri_params = parse_uri_parameters(uri)
+               var path = "/"
+               for part in path_parts do
+                       if part isa UriString then
+                               path /= part.string
+                       else if part isa UriParam then
+                               path /= uri_params.get_or_default(part.name, part.name)
+                       end
+               end
+               return path.simplify_path
+       end
+
+       # Extract parameter values from `uri`.
+       # ~~~
+       # var route = new AppParamRoute("/user/:userId/items/:itemId")
+       # var params = route.parse_uri_parameters("/user/10/items/i125/desc")
+       # assert params["userId"] == "10"
+       # assert params["itemId"] == "i125"
+       # assert params.length == 2
+       #
+       # params = route.parse_uri_parameters("/")
+       # assert params.is_empty
+       # ~~~
+       fun parse_uri_parameters(uri: String): Map[String, String] do
+               var res = new HashMap[String, String]
+               if path_parts.is_empty then return res
+               var parts = uri.split("/")
+               for i in [0 .. path_parts.length[ do
+                       if i >= parts.length then return res
+                       var ppart = path_parts[i]
+                       var part = parts[i]
+                       if not ppart.match(part) then return res
+                       if ppart isa UriParam then
+                               res[ppart.name] = part
+                       end
+               end
+               return res
+       end
+
+       private var path_parts = new Array[UriPart]
+end
+
+# Route with glob.
+#
+# Route variable part is suffixed with a star `*`:
+# ~~~
+# var route = new AppGlobRoute("/*")
+# assert route.match("/")
+# assert route.match("/user")
+# assert route.match("/user/10")
+# ~~~
+#
+# Glob routes can be combined with parameters:
+# ~~~
+# route = new AppGlobRoute("/user/:id/*")
+# assert not route.match("/user")
+# assert route.match("/user/10")
+# assert route.match("/user/10/profile")
+# ~~~
+#
+# Note that the star can be used directly on the end of an URI part:
+# ~~~
+# route = new AppGlobRoute("/user*")
+# assert route.match("/user")
+# assert route.match("/username")
+# assert route.match("/user/10/profile")
+# assert not route.match("/foo")
+# ~~~
+#
+# For now, stars cannot be used inside a route, use URI parameters instead.
+class AppGlobRoute
+       super AppParamRoute
+
+       # Path without the trailing `*`.
+       # ~~~
+       # var route = new AppGlobRoute("/user/:id/*")
+       # assert route.resolve_path("/user/10/profile") == "/user/10"
+       #
+       # route = new AppGlobRoute("/user/:userId/items/:itemId*")
+       # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
+       # ~~~
+       redef fun resolve_path(uri) do
+               var path = super
+               if path.has_suffix("*") then
+                       return path.substring(0, path.length - 1).simplify_path
+               end
+               return path.simplify_path
+       end
+
+       redef fun match(uri) do
+               var path = resolve_path(uri)
+               return uri.has_prefix(path.substring(0, path.length - 1))
+       end
+end
+
+# A String that compose an URI.
+#
+# In practice, UriPart can be parameters or static strings.
+private interface UriPart
+       # Does `self` matches a part of the uri?
+       fun match(uri_part: String): Bool is abstract
+end
+
+# An uri parameter string like `:id`.
+private class UriParam
+       super UriPart
+
+       # Param `name` in the route uri.
+       var name: String
+
+       # Parameters match everything.
+       redef fun match(part) do return not part.is_empty
+
+       redef fun to_s do return name
+end
+
+# A static uri string like `users`.
+private class UriString
+       super UriPart
+
+       # Uri part string.
+       var string: String
+
+       # Empty strings match everything otherwise matching is based on string equality.
+       redef fun match(part) do return string.is_empty or string == part
+
+       redef fun to_s do return string
+end
diff --git a/lib/popcorn/popcorn.nit b/lib/popcorn/popcorn.nit
new file mode 100644 (file)
index 0000000..887c122
--- /dev/null
@@ -0,0 +1,89 @@
+# 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.
+
+# Application server abstraction on top of nitcorn.
+module popcorn
+
+import nitcorn
+import pop_middlewares
+intrude import pop_handlers
+
+# App acts like a wrapper around a nitcorn `Action`.
+redef class App
+       super Action
+
+       # Do not show anything on console
+       var quiet = false is writable
+
+       # Start listening on `host:port`.
+       fun listen(host: String, port: Int) do
+               var iface = "{host}:{port}"
+               var vh = new VirtualHost(iface)
+
+               vh.routes.add new Route("/", self)
+
+               var fac = new HttpFactory.and_libevent
+               fac.config.virtual_hosts.add vh
+
+               if not quiet then
+                       print "Launching server on http://{iface}/"
+               end
+               fac.run
+       end
+
+       # Handle request from nitcorn
+       redef fun answer(req, uri) do
+               uri = uri.simplify_path
+               var res = new HttpResponse(404)
+               for route, handler in handlers do
+                       handler.handle(route, uri, req, res)
+               end
+               if not res.sent then
+                       res.send(error_tpl(res.status_code, res.status_message), 404)
+               end
+               res.session = req.session
+               return res
+       end
+
+       #
+       fun error_tpl(status: Int, message: nullable String): Template do
+               return new ErrorTpl(status, message)
+       end
+end
+
+#
+class ErrorTpl
+       super Template
+
+       #
+       var status: Int
+
+       #
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+
+end
diff --git a/lib/popcorn/test_pop_routes.nit b/lib/popcorn/test_pop_routes.nit
new file mode 100644 (file)
index 0000000..9ad37a5
--- /dev/null
@@ -0,0 +1,166 @@
+# 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.
+
+module test_pop_routes is test_suite
+
+import pop_routes
+import test_suite
+
+class TestAppRoute
+       super TestSuite
+
+       fun test_root_match_only_one_uri do
+               var r = new AppRoute("/")
+               assert r.match("")
+               assert r.match("/")
+               assert not r.match("/user")
+       end
+
+       fun test_strict_route_match_only_one_uri do
+               var r = new AppRoute("/user")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/")
+               assert not r.match("/user/10")
+               assert not r.match("/foo")
+       end
+end
+
+class TestAppParamRoute
+       super TestSuite
+
+       fun test_param_route_match_good_uri_params_1 do
+               var r = new AppParamRoute("/:id")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/")
+               assert not r.match("/user/10")
+       end
+
+       fun test_param_route_match_good_uri_params_2 do
+               var r = new AppParamRoute("/user/:id")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/user/")
+               assert r.match("/user/10")
+               assert r.match("/user/10/")
+               assert not r.match("/user/10/profile")
+       end
+
+       fun test_param_route_match_good_uri_params_3 do
+               var r = new AppParamRoute("/user/:id/profile")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/user/")
+               assert not r.match("/user/10")
+               assert not r.match("/user/10/")
+               assert r.match("/user/10/profile")
+               assert r.match("/user/10/profile/")
+               assert not r.match("/user/10/profile/settings")
+               assert not r.match("/user/10/foo")
+       end
+
+       fun test_param_route_match_good_uri_params_4 do
+               var r = new AppParamRoute("/:id/:foo")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/user/")
+               assert r.match("/user/10")
+               assert r.match("/user/10/")
+               assert not r.match("/user/10/10")
+       end
+
+       fun test_param_route_match_good_uri_params_5 do
+               var r = new AppParamRoute("/user/:id/:foo")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/foo")
+               assert not r.match("/user/10")
+               assert r.match("/user/10/10")
+               assert r.match("/user/10/10/")
+               assert not r.match("/user/10/10/profile")
+       end
+
+       fun test_param_route_match_good_uri_params_6 do
+               var r = new AppParamRoute("/user/:id/settings/:foo")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/foo")
+               assert not r.match("/user/10")
+               assert not r.match("/user/10/10")
+               assert not r.match("/user/10/10/")
+               assert not r.match("/user/10/10/profile")
+               assert r.match("/user/10/settings/profile")
+               assert r.match("/user/10/settings/profile/")
+               assert not r.match("/user/10/settings/profile/10")
+       end
+end
+
+class TestRouteMatching
+       super TestSuite
+
+       fun test_glob_route_match_good_uri_prefix1 do
+               var r = new AppGlobRoute("/*")
+               assert r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_match_good_uri_prefix2 do
+               var r = new AppGlobRoute("/user/*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_match_good_uri_prefix3 do
+               var r = new AppGlobRoute("/user*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_work_with_parameters_1 do
+               var r = new AppGlobRoute("/:id/*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+               assert r.match("/user/10/profile")
+       end
+
+       fun test_glob_route_work_with_parameters_2 do
+               var r = new AppGlobRoute("/:id*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_work_with_parameters_3 do
+               var r = new AppGlobRoute("/user/:id/*")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert r.match("/user/10")
+               assert r.match("/user/10/profile")
+       end
+
+       fun test_glob_route_work_with_parameters_4 do
+               var r = new AppGlobRoute("/user/:id*")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert r.match("/user/10")
+               assert r.match("/user/10/profile")
+       end
+end
diff --git a/lib/popcorn/tests/Makefile b/lib/popcorn/tests/Makefile
new file mode 100644 (file)
index 0000000..7e78098
--- /dev/null
@@ -0,0 +1,21 @@
+# Copyright 2013 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.
+
+all: tests
+
+check: clean
+       ./tests.sh
+
+clean:
+       rm -rf out/
diff --git a/lib/popcorn/tests/base_tests.nit b/lib/popcorn/tests/base_tests.nit
new file mode 100644 (file)
index 0000000..a56a33d
--- /dev/null
@@ -0,0 +1,63 @@
+# 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 popcorn
+import pthreads
+
+redef class Sys
+       var test_host = "localhost"
+
+       # Return a new port for each instance
+       fun test_port: Int do
+               srand
+               return 10000+20000.rand
+       end
+end
+
+class AppThread
+       super Thread
+
+       var host: String
+       var port: Int
+       var app: App
+
+       redef fun main
+       do
+               # Hide testing concept to force nitcorn to actually run
+               "NIT_TESTING".setenv("false")
+               app.quiet = true
+               app.listen(host, port)
+               return null
+       end
+end
+
+class ClientThread
+       super Thread
+
+       var host: String
+       var port: Int
+
+       redef fun main do return null
+
+       # Regex to catch and hide the port from the output to get consistent results
+       var host_re: Regex = "localhost:\[0-9\]+".to_re
+
+       fun system(cmd: String, title: nullable String)
+       do
+               title = title or else cmd
+               title = title.replace(host_re, "localhost:*****")
+               print "\n[Client] {title}"
+               sys.system cmd
+       end
+end
diff --git a/lib/popcorn/tests/res/test_example_advanced_logger.res b/lib/popcorn/tests/res/test_example_advanced_logger.res
new file mode 100644 (file)
index 0000000..7e56183
--- /dev/null
@@ -0,0 +1,16 @@
+
+[Client] curl -s localhost:*****/
+GET / \e[32m200\e[m (0.0s)
+Hello World!
+[Client] curl -s localhost:*****/about
+GET /about \e[33m404\e[m (0.0s)
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_angular.res b/lib/popcorn/tests/res/test_example_angular.res
new file mode 100644 (file)
index 0000000..c07813f
--- /dev/null
@@ -0,0 +1,7 @@
+
+[Client] curl -s localhost:*****/counter
+{"label":"Visitors","value":0}
+[Client] curl -s localhost:*****/counter -X POST
+{"label":"Visitors","value":1}
+[Client] curl -s localhost:*****/counter
+{"label":"Visitors","value":1}
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_glob_route.res b/lib/popcorn/tests/res/test_example_glob_route.res
new file mode 100644 (file)
index 0000000..1e91986
--- /dev/null
@@ -0,0 +1,42 @@
+
+[Client] curl -s localhost:*****/user/Morriar/item/10
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/user/Morriar/item/10/
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/user/Morriar/item/10/profile
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/user/Morriar/item/10/profile/settings
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_hello.res b/lib/popcorn/tests/res/test_example_hello.res
new file mode 100644 (file)
index 0000000..194fb36
--- /dev/null
@@ -0,0 +1,29 @@
+
+[Client] curl -s localhost:*****
+<h1>Hello World!</h1>
+[Client] curl -s localhost:*****/
+<h1>Hello World!</h1>
+[Client] curl -s localhost:*****///////////
+<h1>Hello World!</h1>
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_html_error_handler.res b/lib/popcorn/tests/res/test_example_html_error_handler.res
new file mode 100644 (file)
index 0000000..3015280
--- /dev/null
@@ -0,0 +1,23 @@
+
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>An error occurred!</title>
+               </head>
+               <body>
+               <h1>404 An error occurred!</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/about
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>An error occurred!</title>
+               </head>
+               <body>
+               <h1>404 An error occurred!</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_param_route.res b/lib/popcorn/tests/res/test_example_param_route.res
new file mode 100644 (file)
index 0000000..f0b898f
--- /dev/null
@@ -0,0 +1,38 @@
+
+[Client] curl -s localhost:*****/Morriar
+Hello Morriar
+[Client] curl -s localhost:*****//
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found
+Hello not_found
+[Client] curl -s localhost:*****/not_found/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_post.res b/lib/popcorn/tests/res/test_example_post.res
new file mode 100644 (file)
index 0000000..72589e2
--- /dev/null
@@ -0,0 +1,38 @@
+
+[Client] curl -s localhost:*****/ -X POST
+URI: /
+Body: 
+
+[Client] curl -s localhost:*****/ --data 'user'
+POST Error: user format error on user
+URI: /
+Body: user
+
+[Client] curl -s localhost:*****/ --data 'user=Morriar'
+URI: /
+Body: user=Morriar
+user: Morriar
+
+[Client] curl -s localhost:*****/ --data 'user=&order=desc'
+URI: /
+Body: user=&order=desc
+user: 
+order: desc
+
+[Client] curl -s localhost:*****/ --data 'user=Morriar&order=desc'
+URI: /
+Body: user=Morriar&order=desc
+user: Morriar
+order: desc
+
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_query_string.res b/lib/popcorn/tests/res/test_example_query_string.res
new file mode 100644 (file)
index 0000000..699b0db
--- /dev/null
@@ -0,0 +1,24 @@
+
+[Client] curl -s localhost:*****/
+URI: /
+Query string: 
+
+[Client] curl -s localhost:*****/?user=Morriar
+URI: /
+Query string: user=Morriar
+user: Morriar
+
+[Client] curl -s localhost:*****/?reload
+URI: /
+Query string: reload
+
+[Client] curl -s localhost:*****/?foo\&bar=baz
+URI: /
+Query string: foo&bar=baz
+bar: baz
+
+[Client] curl -s localhost:*****/?items=10\&order=asc
+URI: /
+Query string: items=10&order=asc
+items: 10
+order: asc
diff --git a/lib/popcorn/tests/res/test_example_router.res b/lib/popcorn/tests/res/test_example_router.res
new file mode 100644 (file)
index 0000000..fb222c3
--- /dev/null
@@ -0,0 +1,48 @@
+
+[Client] curl -s localhost:*****
+Site Home
+[Client] curl -s localhost:*****/
+Site Home
+[Client] curl -s localhost:*****/user
+User logged
+User Home
+[Client] curl -s localhost:*****/user/
+User logged
+User Home
+[Client] curl -s localhost:*****/user/profile
+User logged
+User Profile
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/not_found
+User logged
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/products/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_session.res b/lib/popcorn/tests/res/test_example_session.res
new file mode 100644 (file)
index 0000000..4ca758e
--- /dev/null
@@ -0,0 +1,41 @@
+
+[Client] curl -s localhost:*****/
+               <p>Is logged: false</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>
+[Client] curl -s localhost:*****/ -X POST
+
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/products/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_simple_error_handler.res b/lib/popcorn/tests/res/test_example_simple_error_handler.res
new file mode 100644 (file)
index 0000000..67e5a72
--- /dev/null
@@ -0,0 +1,5 @@
+
+[Client] curl -s localhost:*****/
+Hello World!
+[Client] curl -s localhost:*****/about
+An error occurred!
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_simple_logger.res b/lib/popcorn/tests/res/test_example_simple_logger.res
new file mode 100644 (file)
index 0000000..9bc50e3
--- /dev/null
@@ -0,0 +1,16 @@
+
+[Client] curl -s localhost:*****/
+Request Logged
+Hello World!
+[Client] curl -s localhost:*****/about
+Request Logged
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_static.res b/lib/popcorn/tests/res/test_example_static.res
new file mode 100644 (file)
index 0000000..7f07ed8
--- /dev/null
@@ -0,0 +1,73 @@
+
+[Client] curl -s localhost:*****/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/static/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_static_multiple.res b/lib/popcorn/tests/res/test_example_static_multiple.res
new file mode 100644 (file)
index 0000000..1b14f14
--- /dev/null
@@ -0,0 +1,113 @@
+
+[Client] curl -s localhost:*****/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/
+Warning: Headers already sent!
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/static/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/static/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/static/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/static/
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/static/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_router.res b/lib/popcorn/tests/res/test_router.res
new file mode 100644 (file)
index 0000000..37be3a6
--- /dev/null
@@ -0,0 +1,50 @@
+
+[Client] curl -s localhost:*****
+/
+[Client] curl -s localhost:*****/
+/
+[Client] curl -s localhost:*****/user
+/user
+[Client] curl -s localhost:*****/user/
+/user
+[Client] curl -s localhost:*****/user/settings
+/user/settings
+[Client] curl -s localhost:*****/products
+/products
+[Client] curl -s localhost:*****/products/
+/products
+[Client] curl -s localhost:*****/products/list
+/products/list
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/products/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_routes.res b/lib/popcorn/tests/res/test_routes.res
new file mode 100644 (file)
index 0000000..0c1abfa
--- /dev/null
@@ -0,0 +1,49 @@
+
+[Client] curl -s localhost:*****
+/
+[Client] curl -s localhost:*****/
+/
+[Client] curl -s localhost:*****/misc
+/misc/everything
+[Client] curl -s localhost:*****/misc/foo
+/misc/everything
+[Client] curl -s localhost:*****/misc/foo/bar
+/misc/everything
+[Client] curl -s localhost:*****/misc/foo/baz
+/misc/everything
+[Client] curl -s localhost:*****/user
+/user
+[Client] curl -s localhost:*****/user/
+/user
+[Client] curl -s localhost:*****/user/id
+/user/id
+[Client] curl -s localhost:*****/user/id/profile
+/user/id/profile
+[Client] curl -s localhost:*****/user/id/misc/foo
+/user/id/misc/everything
+[Client] curl -s localhost:*****/user/id/misc/foo/bar
+/user/id/misc/everything
+[Client] curl -s localhost:*****/user/id/misc/foo/bar/baz
+/user/id/misc/everything
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/id/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/test_example_advanced_logger.nit b/lib/popcorn/tests/test_example_advanced_logger.nit
new file mode 100644 (file)
index 0000000..41193d2
--- /dev/null
@@ -0,0 +1,47 @@
+# 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.
+
+import example_advanced_logger
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use("/*", new LogHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_angular.nit b/lib/popcorn/tests/test_example_angular.nit
new file mode 100644 (file)
index 0000000..d1ab509
--- /dev/null
@@ -0,0 +1,47 @@
+# 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.
+
+import example_angular
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/counter"
+               system "curl -s {host}:{port}/counter -X POST"
+               system "curl -s {host}:{port}/counter"
+               return null
+       end
+end
+
+var app = new App
+app.use("/counter", new CounterAPI)
+app.use("/*", new StaticHandler("www/"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_glob_route.nit b/lib/popcorn/tests/test_example_glob_route.nit
new file mode 100644 (file)
index 0000000..d1ca6c8
--- /dev/null
@@ -0,0 +1,51 @@
+# 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.
+
+import example_glob_route
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/user/Morriar/item/10"
+               system "curl -s {host}:{port}/user/Morriar/item/10/"
+               system "curl -s {host}:{port}/user/Morriar/item/10/profile"
+               system "curl -s {host}:{port}/user/Morriar/item/10/profile/settings"
+
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/not_found/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_hello.nit b/lib/popcorn/tests/test_example_hello.nit
new file mode 100644 (file)
index 0000000..d7327e3
--- /dev/null
@@ -0,0 +1,48 @@
+# 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.
+
+import example_hello
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}///////////"
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/not_found/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_html_error_handler.nit b/lib/popcorn/tests/test_example_html_error_handler.nit
new file mode 100644 (file)
index 0000000..95abb7a
--- /dev/null
@@ -0,0 +1,45 @@
+# 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.
+
+import example_html_error_handler
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_param_route.nit b/lib/popcorn/tests/test_example_param_route.nit
new file mode 100644 (file)
index 0000000..541fde0
--- /dev/null
@@ -0,0 +1,49 @@
+# 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.
+
+import example_param_route
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/Morriar"
+               system "curl -s {host}:{port}//"
+
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/not_found/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_post.nit b/lib/popcorn/tests/test_example_post.nit
new file mode 100644 (file)
index 0000000..bf5330e
--- /dev/null
@@ -0,0 +1,50 @@
+# 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.
+
+import example_post_handler
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/ -X POST"
+               system "curl -s {host}:{port}/ --data 'user'"
+               system "curl -s {host}:{port}/ --data 'user=Morriar'"
+               system "curl -s {host}:{port}/ --data 'user=\&order=desc'"
+               system "curl -s {host}:{port}/ --data 'user=Morriar\&order=desc'"
+
+               system "curl -s {host}:{port}/"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_query_string.nit b/lib/popcorn/tests/test_example_query_string.nit
new file mode 100644 (file)
index 0000000..6c489ba
--- /dev/null
@@ -0,0 +1,48 @@
+# 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.
+
+import example_query_string
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/?user=Morriar"
+               system "curl -s {host}:{port}/?reload"
+               system "curl -s {host}:{port}/?foo\\&bar=baz"
+               system "curl -s {host}:{port}/?items=10\\&order=asc"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_router.nit b/lib/popcorn/tests/test_example_router.nit
new file mode 100644 (file)
index 0000000..f9ab8df
--- /dev/null
@@ -0,0 +1,58 @@
+# 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.
+
+import example_router
+import base_tests
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/user"
+               system "curl -s {host}:{port}/user/"
+               system "curl -s {host}:{port}/user/profile"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/not_found"
+               system "curl -s {host}:{port}/products/not_found"
+               return null
+       end
+end
+
+var user_router = new Router
+user_router.use("/*", new UserLogger)
+user_router.use("/", new UserHome)
+user_router.use("/profile", new UserProfile)
+
+var app = new App
+app.use("/", new AppHome)
+app.use("/user", user_router)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_session.nit b/lib/popcorn/tests/test_example_session.nit
new file mode 100644 (file)
index 0000000..b7bedc3
--- /dev/null
@@ -0,0 +1,50 @@
+# 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.
+
+import example_session
+import base_tests
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/ -X POST"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/not_found"
+               system "curl -s {host}:{port}/products/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_simple_error_handler.nit b/lib/popcorn/tests/test_example_simple_error_handler.nit
new file mode 100644 (file)
index 0000000..ce671f0
--- /dev/null
@@ -0,0 +1,46 @@
+# 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.
+
+import example_simple_error_handler
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_simple_logger.nit b/lib/popcorn/tests/test_example_simple_logger.nit
new file mode 100644 (file)
index 0000000..50f6af9
--- /dev/null
@@ -0,0 +1,46 @@
+# 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.
+
+import example_simple_logger
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use("/*", new LogHandler)
+app.use("/", new HelloHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_static.nit b/lib/popcorn/tests/test_example_static.nit
new file mode 100644 (file)
index 0000000..e23a7be
--- /dev/null
@@ -0,0 +1,52 @@
+# 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.
+
+import base_tests
+import example_static
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/css/style.css"
+               system "curl -s {host}:{port}/js/app.js"
+               system "curl -s {host}:{port}/hello.html"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/css/not_found.nit"
+               system "curl -s {host}:{port}/static/css/not_found.nit"
+               system "curl -s {host}:{port}/not_found.nit"
+
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new StaticHandler("../examples/static_files/public/"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_static_multiple.nit b/lib/popcorn/tests/test_example_static_multiple.nit
new file mode 100644 (file)
index 0000000..5e99ae1
--- /dev/null
@@ -0,0 +1,60 @@
+# 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.
+
+import base_tests
+import example_static_multiple
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/css/style.css"
+               system "curl -s {host}:{port}/js/app.js"
+               system "curl -s {host}:{port}/hello.html"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/static/css/style.css"
+               system "curl -s {host}:{port}/static/js/app.js"
+               system "curl -s {host}:{port}/static/hello.html"
+               system "curl -s {host}:{port}/static/"
+
+               system "curl -s {host}:{port}/css/not_found.nit"
+               system "curl -s {host}:{port}/static/css/not_found.nit"
+               system "curl -s {host}:{port}/not_found.nit"
+
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new StaticHandler("../examples/static_files/public/"))
+app.use("/", new StaticHandler("../examples/static_files/files/"))
+app.use("/static", new StaticHandler("../examples/static_files/public/"))
+app.use("/static", new StaticHandler("../examples/static_files/files/"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_router.nit b/lib/popcorn/tests/test_router.nit
new file mode 100644 (file)
index 0000000..915ce4a
--- /dev/null
@@ -0,0 +1,73 @@
+# 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.
+
+import base_tests
+
+class TestHandler
+       super Handler
+
+       var marker: String
+
+       redef fun get(req, res) do res.send marker
+end
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/user"
+               system "curl -s {host}:{port}/user/"
+               system "curl -s {host}:{port}/user/settings"
+               system "curl -s {host}:{port}/products"
+               system "curl -s {host}:{port}/products/"
+               system "curl -s {host}:{port}/products/list"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/not_found"
+               system "curl -s {host}:{port}/products/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new TestHandler("/"))
+app.use("/about", new TestHandler("/about"))
+
+var router1 = new App
+router1.use("/", new TestHandler("/user"))
+router1.use("/settings", new TestHandler("/user/settings"))
+app.use("/user", router1)
+
+var router2 = new App
+router2.use("/", new TestHandler("/products"))
+router2.use("/list", new TestHandler("/products/list"))
+app.use("/products", router2)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_routes.nit b/lib/popcorn/tests/test_routes.nit
new file mode 100644 (file)
index 0000000..2db660a
--- /dev/null
@@ -0,0 +1,76 @@
+# 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.
+
+import base_tests
+
+class TestHandler
+       super Handler
+
+       var marker: String
+
+       redef fun get(req, res) do res.send marker
+end
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/misc"
+               system "curl -s {host}:{port}/misc/foo"
+               system "curl -s {host}:{port}/misc/foo/bar"
+               system "curl -s {host}:{port}/misc/foo/baz"
+
+               system "curl -s {host}:{port}/user"
+               system "curl -s {host}:{port}/user/"
+               system "curl -s {host}:{port}/user/id"
+               system "curl -s {host}:{port}/user/id/profile"
+               system "curl -s {host}:{port}/user/id/misc/foo"
+               system "curl -s {host}:{port}/user/id/misc/foo/bar"
+               system "curl -s {host}:{port}/user/id/misc/foo/bar/baz"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/id/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new TestHandler("/"))
+app.use("/user", new TestHandler("/user"))
+app.use("/misc/*", new TestHandler("/misc/everything"))
+app.use("/user/:id", new TestHandler("/user/id"))
+app.use("/user/:id/profile", new TestHandler("/user/id/profile"))
+app.use("/user/:id/misc/*", new TestHandler("/user/id/misc/everything"))
+
+var host = test_host
+var port = test_port
+
+# First, launch a server in the background
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+# Then, launch a client running test requests
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+# Force quit the server
+exit 0
diff --git a/lib/popcorn/tests/tests.sh b/lib/popcorn/tests/tests.sh
new file mode 100755 (executable)
index 0000000..4406058
--- /dev/null
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# 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.
+
+BIN=../bin
+OUT=./out
+RES=./res
+
+NITC=../../../bin/nitc
+
+compile() {
+       local test="$1"
+       $NITC $test.nit -o $OUT/$test.bin 1>&2 2> $OUT/$test.cmp_err
+}
+
+test_prog()
+{
+       local test="$1"
+
+       chmod +x $OUT/$test.bin 2> $OUT/$test.err
+       $OUT/$test.bin > $OUT/$test.res 2> $OUT/$test.err
+
+       diff $OUT/$test.res $RES/$test.res > $OUT/$test.diff 2> /dev/null
+}
+
+# return
+#  0 if the sav not exists
+#  1 if the file does match
+#  2 if the file does not match
+check_result() {
+       local test="$1"
+
+       if [ -s "$OUT/$test.cmp_err" ]; then
+               return 0
+       elif [ -s "$OUT/$test.err" ]; then
+               return 1
+       elif [ ! -r "$RES/$test.res" ]; then
+               return 2
+       elif [ -s "$OUT/$test.diff" ]; then
+               return 3
+       else
+               return 4
+       fi
+}
+
+echo "Testing..."
+echo ""
+
+rm -rf $OUT 2>/dev/null
+mkdir $OUT 2>/dev/null
+
+all=0
+ok=0
+ko=0
+sk=0
+
+for file in `ls test_*.nit`; do
+       ((all++))
+       test="${file%.*}"
+       echo -n "* $test: "
+
+       compile $test
+       test_prog $test
+       check_result $test
+
+       case "$?" in
+               0)
+                       echo "compile error (cat $OUT/$test.cmp_err)"
+                       ((ko++))
+                       ;;
+               1)
+                       echo "error (cat $OUT/$test.cmp_err)"
+                       ((ko++))
+                       ;;
+               2)
+                       echo "skip ($test.res not found)"
+                       ((sk++))
+                       continue;;
+               3)
+                       echo "error (diff $OUT/$test.res $RES/$test.res)"
+                       ((ko++))
+                       ;;
+               4)
+                       echo "success"
+                       ((ok++))
+                       ;;
+
+       esac
+done
+echo ""
+echo "==> success $ok/$all ($ko tests failed, $sk skipped)"
+
+# return result
+test "$ok" == "$all"
index 4541428..3554d22 100644 (file)
@@ -55,7 +55,7 @@ end
 # then using a reference.
 class SerializerCache
        # Map of already serialized objects to the reference id
-       private var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int]
+       protected var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int]
 
        # Is `object` known?
        fun has_object(object: Serializable): Bool do return sent.keys.has(object)
@@ -88,7 +88,7 @@ end
 # Used by `Deserializer` to find already deserialized objects by their reference.
 class DeserializerCache
        # Map of references to already deserialized objects.
-       private var received: Map[Int, Object] = new StrictHashMap[Int, Object]
+       protected var received: Map[Int, Object] = new StrictHashMap[Int, Object]
 
        # Is there an object associated to `id`?
        fun has_id(id: Int): Bool do return received.keys.has(id)
index d0bc77d..615c45f 100644 (file)
@@ -37,7 +37,7 @@ import template
 # A macro identifier is valid if:
 #
 # * starts with an uppercase letter
-# * contains only numers, uppercase letters or '_'
+# * contains only numbers, uppercase letters or '_'
 #
 # See `String::is_valid_macro_name` for more details.
 #
index 655005d..95995bf 100644 (file)
@@ -1,5 +1,4 @@
-# This is a full install of Nit on a debian base.
-# Full because most dependencies are installed so that most tests can be run
+# This is a basic install of Nit on a debian base.
 
 FROM debian:jessie
 MAINTAINER Jean Privat <jean@pryen.org>
@@ -31,36 +30,6 @@ RUN git clone https://github.com/nitlang/nit.git /root/nit \
        && ccache -C \
        && rm -rf .git
 
-# Dependencies for more libs and tests
-RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
-               # Packages needed for lib/
-               libcurl4-openssl-dev \
-               libegl1-mesa-dev \
-               libevent-dev \
-               libgles1-mesa-dev \
-               libgles2-mesa-dev \
-               libgtk-3-dev \
-               libncurses5-dev \
-               libsdl-image1.2-dev \
-               libsdl-ttf2.0-dev \
-               libsdl1.2-dev \
-               libsdl2-dev \
-               libsqlite3-dev \
-               libx11-dev \
-               libxdg-basedir-dev \
-               # Packages needed for platforms and FFI
-               default-jdk \
-               libopenmpi-dev \
-               clang \
-               # TODO neo4j android emscripten test_glsl_validation
-       && rm -rf /var/lib/apt/lists/*
-
-# Run tests
-RUN cd /root/nit/tests \
-       && ./testfull.sh || true \
-       && rm -rf out/ alt/*.nit \
-       && ccache -C
-# TODO: nitunits
-
-WORKDIR /root/nit
-ENTRYPOINT [ "bash" ]
+ENV NIT_DIR /root/nit
+ENV PATH $NIT_DIR/bin:$PATH
+WORKDIR $NIT_DIR
diff --git a/misc/docker/README.md b/misc/docker/README.md
new file mode 100644 (file)
index 0000000..d255285
--- /dev/null
@@ -0,0 +1,64 @@
+# Supported tags and respective Dockerfile links
+
+* [latest](https://github.com/nitlang/nit/blob/master/misc/docker/Dockerfile)
+* [full](https://github.com/nitlang/nit/blob/master/misc/docker/full/Dockerfile)
+
+## What is Nit?
+
+Nit is an expressive language with a script-like syntax, a friendly type-system and aims at elegance, simplicity and intuitiveness.
+
+Nit has a simple straightforward style and can usually be picked up quickly, particularly by anyone who has programmed before.
+While object-oriented, it allows procedural styles.
+
+More information on
+
+* Website <http://www.nitlanguage.org>
+* Github <https://github.com/nitlang/nit>
+* Chatroom <https://gitter.im/nitlang/nit>
+
+## How to use this image
+
+You can use these images to build then run your programs.
+
+### Experimenting with Nit
+
+~~~
+host$ docker run -ti nitlang/nit
+root@ce9b671dd9fc:/root/nit# nitc examples/hello_world.nit
+root@ce9b671dd9fc:/root/nit# ./hello_world
+hello world
+~~~
+
+### Build and Run Programs
+
+In your Dockerfile, write something like:
+
+~~~Dockerfile
+FROM nitlang/nit
+
+# Create a workdir
+RUN mkdir -p /root/work
+WORKDIR /root/work
+
+# Copy the source code in /root/work/
+COPY . /root/work/
+
+# Compile
+RUN nitc src/hello.nit --dir . \
+       # Clear disk space
+       && ccache -C
+# You can also use a Makefile or any build system you want.
+
+# Run
+CMD ["./hello"]
+~~~
+
+Then, build and execute
+
+~~~
+host$ docker build -t nithello .
+host$ docker run --rm nithello
+hello!
+~~~
+
+See the full example at <https://github.com/nitlang/nit/blob/master/misc/docker/hello/Dockerfile>
diff --git a/misc/docker/full/Dockerfile b/misc/docker/full/Dockerfile
new file mode 100644 (file)
index 0000000..df0d214
--- /dev/null
@@ -0,0 +1,76 @@
+# This is a full install of Nit on a debian base.
+# Full because most dependencies are installed so that most tests can be run
+
+FROM nitlang/nit:latest
+MAINTAINER Jean Privat <jean@pryen.org>
+
+# Dependencies for more libs and tests
+RUN dpkg --add-architecture i386 \
+       && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+               # Packages needed for lib/
+               libcurl4-openssl-dev \
+               libegl1-mesa-dev \
+               libevent-dev \
+               libgles1-mesa-dev \
+               libgles2-mesa-dev \
+               libgtk-3-dev \
+               libncurses5-dev \
+               libpq-dev \
+               libsdl-image1.2-dev \
+               libsdl-ttf2.0-dev \
+               libsdl1.2-dev \
+               libsdl2-dev \
+               libsqlite3-dev \
+               libx11-dev \
+               libxdg-basedir-dev \
+               postgresql \
+               # Packages needed for contrib, platforms and FFI
+               ant \
+               clang \
+               default-jdk \
+               file \
+               inkscape \
+               libopenmpi-dev \
+               unzip \
+               # Android
+               libc6:i386 \
+               libstdc++6:i386 \
+               zlib1g:i386 \
+               # TODO neo4j emscripten test_glsl_validation
+       && rm -rf /var/lib/apt/lists/*
+
+# Install android sdk/ndk
+RUN mkdir -p /opt \
+       && cd /opt \
+       # Android SDK
+       && curl https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz -o android-sdk-linux.tgz \
+       && tar xzf android-sdk-linux.tgz \
+       && rm android-sdk-linux.tgz \
+       && echo y | android-sdk-linux/tools/android update sdk -a --no-ui --filter \
+               # Hardcode minimal known working things
+               platform-tools,build-tools-22.0.1,android-22,android-10 \
+       # Android NDK
+       && curl http://dl.google.com/android/repository/android-ndk-r11c-linux-x86_64.zip -o android-ndk.zip \
+       && unzip -q android-ndk.zip \
+       && ln -s android-ndk-r11c android-ndk \
+       && rm android-ndk.zip \
+       && printf "PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_NDK\nexport PATH\n" >> "/etc/profile.d/android.sh"
+
+# Setup environment variables
+
+ENV ANDROID_HOME /opt/android-sdk-linux
+ENV ANDROID_NDK /opt/android-ndk
+ENV PATH $PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_NDK
+
+# Run tests
+RUN cd /root/nit/tests \
+       # Basic tests
+       && ./testfull.sh || true \
+       && rm -rf out/ alt/*.nit \
+       # Nitunits
+       && ../bin/nitunit ../lib ../contrib || true \
+       && rm -rf .nitunit \
+       && ccache -C
+
+WORKDIR /root/nit
+ENTRYPOINT [ "bash" ]
diff --git a/misc/docker/hello/Dockerfile b/misc/docker/hello/Dockerfile
new file mode 100644 (file)
index 0000000..c0a018b
--- /dev/null
@@ -0,0 +1,17 @@
+FROM nitlang/nit
+
+# Create a workdir
+RUN mkdir -p /root/work
+WORKDIR /root/work
+
+# Copy the source code in /root/work/
+COPY . /root/work/
+
+# Compile
+RUN nitc src/hello.nit --dir . \
+        # Clear disk space
+        && ccache -C
+# You can also use a Makefile or what you want
+
+# Say what to run
+CMD ["./hello"]
diff --git a/misc/docker/hello/src/hello.nit b/misc/docker/hello/src/hello.nit
new file mode 100644 (file)
index 0000000..b732142
--- /dev/null
@@ -0,0 +1 @@
+print "hello"
index 4f9ad61..0576201 100644 (file)
@@ -169,6 +169,34 @@ class TestFoo
 end
 ~~~~
 
+## Black Box Testing
+
+Sometimes, it is easier to validate a `TestCase` by comparing its output with a text file containing the expected result.
+
+For each TestCase `test_bar` of a TestSuite `test_mod.nit`, if the corresponding file `test_mod.sav/test_bar.res` exists, then the output of the test is compared with the file.
+
+The `diff(1)` command is used to perform the comparison.
+The test is failed if non-zero is returned by `diff`.
+
+~~~
+module test_mod is test_suite
+class TestFoo
+       fun test_bar do
+               print "Hello!"
+       end
+end
+~~~
+
+Where `test_mod.sav/test_bar.res` contains
+
+~~~raw
+Hello!
+~~~
+
+If no corresponding `.res` file exists, then the output of the TestCase is ignored.
+
+## Configuring TestSuites
+
 `TestSuites` also provide methods to configure the test run:
 
 `before_test` and `after_test`: methods called before/after each test case.
@@ -237,13 +265,19 @@ With the `--full` option, all imported modules (even those in standard) are also
 ### `-o`, `--output`
 Output name (default is 'nitunit.xml').
 
-### `nitunit` produces a XML file comatible with JUnit.
+`nitunit` produces a XML file compatible with JUnit.
 
 ### `--dir`
 Working directory (default is '.nitunit').
 
 In order to execute the tests, nit files are generated then compiled and executed in the giver working directory.
 
+### `--nitc`
+nitc compiler to use.
+
+By default, nitunit tries to locate the `nitc` program with the environment variable `NITC` or heuristics.
+The option is used to indicate a specific nitc binary.
+
 ### `--no-act`
 Does not compile and run tests.
 
@@ -271,6 +305,13 @@ Also generate test case for private methods.
 ### `--only-show`
 Only display the skeleton, do not write any file.
 
+
+# ENVIRONMENT VARIABLES
+
+### `NITC`
+
+Indicate the specific Nit compiler executable to use. See `--nitc`.
+
 # SEE ALSO
 
 The Nit language documentation and the source code of its tools and libraries may be downloaded from <http://nitlanguage.org>
index d8a04ad..1743179 100644 (file)
@@ -235,6 +235,9 @@ class Catalog
        # Number of warnings and advices
        var warnings = new Counter[MPackage]
 
+       # Number of warnings per 1000 lines of code (w/kloc)
+       var warnings_per_kloc = new Counter[MPackage]
+
        # Documentation score (between 0 and 100)
        var documentation_score = new Counter[MPackage]
 
@@ -254,8 +257,13 @@ class Catalog
        do
                var p = persons.get_or_null(person)
                if p == null then
-                       p = new Person.parse(person)
-                       persons[person] = p
+                       var new_p = new Person.parse(person)
+                       # Maybe, we already have this person in fact?
+                       p = persons.get_or_null(new_p.to_s)
+                       if p == null then
+                               p = new_p
+                               persons[p.to_s] = p
+                       end
                end
                var projs = contrib2proj[p]
                if not projs.has(mpackage) then
@@ -401,6 +409,9 @@ class Catalog
                self.loc[mpackage] = loc
                self.errors[mpackage] = errors
                self.warnings[mpackage] = warnings
+               if loc > 0 then
+                       self.warnings_per_kloc[mpackage] = warnings * 1000 / loc
+               end
                var documentation_score =  (100.0 * doc_score / entity_score).to_i
                self.documentation_score[mpackage] = documentation_score
 
index 65c1c62..2590b0d 100644 (file)
@@ -31,8 +31,38 @@ module model_collect
 
 import model_views
 
+redef class MEntity
+
+       # Collect modifier keywords like `redef`, `private` etc.
+       fun collect_modifiers: Array[String] do
+               return new Array[String]
+       end
+end
+
+redef class MPackage
+       redef fun collect_modifiers do
+               var res = super
+               res.add "package"
+               return res
+       end
+end
+
+redef class MGroup
+       redef fun collect_modifiers do
+               var res = super
+               res.add "group"
+               return res
+       end
+end
+
 redef class MModule
 
+       redef fun collect_modifiers do
+               var res = super
+               res.add "module"
+               return res
+       end
+
        # Collect all transitive imports.
        fun collect_ancestors(view: ModelView): Set[MModule] do
                var res = new HashSet[MModule]
@@ -133,6 +163,8 @@ end
 
 redef class MClass
 
+       redef fun collect_modifiers do return intro.collect_modifiers
+
        # Collect direct parents of `self` with `visibility >= to min_visibility`.
        fun collect_parents(view: ModelView): Set[MClass] do
                var res = new HashSet[MClass]
@@ -404,9 +436,8 @@ redef class MClassDef
                return res
        end
 
-       # Collect modifiers like redef, private etc.
-       fun collect_modifiers: Array[String] do
-               var res = new Array[String]
+       redef fun collect_modifiers do
+               var res = super
                if not is_intro then
                        res.add "redef"
                else
@@ -417,10 +448,13 @@ redef class MClassDef
        end
 end
 
+redef class MProperty
+       redef fun collect_modifiers do return intro.collect_modifiers
+end
+
 redef class MPropDef
-       # Collect modifiers like redef, private, abstract, intern, fun etc.
-       fun collect_modifiers: Array[String] do
-               var res = new Array[String]
+       redef fun collect_modifiers do
+               var res = super
                if not is_intro then
                        res.add "redef"
                else
@@ -440,6 +474,8 @@ redef class MPropDef
                        else
                                res.add "fun"
                        end
+               else if mprop isa MAttributeDef then
+                       res.add "var"
                end
                return res
        end
diff --git a/src/model/model_json.nit b/src/model/model_json.nit
new file mode 100644 (file)
index 0000000..fc4b101
--- /dev/null
@@ -0,0 +1,304 @@
+# 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.
+
+# Make model entities Jsonable.
+#
+# To avoid cycles, every reference from a MEntity to another is replaced by a
+# MEntityRef.
+#
+# How subobjects are retrieved using the MEntityRef is the responsability of the
+# client. Json objects can be returned as this or inflated with concrete objet
+# rather than the refs.
+#
+# TODO consider serialization module?
+module model_json
+
+import model::model_collect
+import json
+import loader
+
+# A reference to another mentity.
+class MEntityRef
+       super MEntity
+
+       # MEntity to link to.
+       var mentity: MEntity
+
+       # Return `self` as a Json Object.
+       #
+       # By default, MEntity references contain only the `full_name` of the Mentity.
+       # You should redefine this method in your client to implement a different behavior.
+       redef fun json do
+               var obj = new JsonObject
+               obj["full_name"] = mentity.full_name
+               return obj
+       end
+end
+
+redef class MEntity
+       super Jsonable
+
+       # Return `self` as a JsonObject.
+       #
+       # By default, every reference to another MEntity is replaced by a pointer
+       # to the MEntity::json_id.
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["name"] = name
+               obj["class_name"] = class_name
+               obj["full_name"] = full_name
+               obj["mdoc"] = mdoc_or_fallback
+               var modifiers = new JsonArray
+               for modifier in collect_modifiers do
+                       modifiers.add modifier
+               end
+               obj["modifiers"] = modifiers
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+end
+
+redef class MDoc
+       super Jsonable
+
+       # Return `self` as a JsonObject.
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["content"] = content.join("\n")
+               obj["location"] = location
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+end
+
+redef class Location
+       super Jsonable
+
+       # Return `self` as a JsonObject.
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["column_end"] = column_end
+               obj["column_start"] = column_start
+               obj["line_end"] = line_end
+               obj["line_start"] = line_start
+               var file = self.file
+               if file != null then
+                       obj["file"] = file.filename
+               end
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+end
+
+redef class MVisibility
+       super Jsonable
+
+       redef fun to_json do return to_s.to_json
+end
+
+redef class MPackage
+
+       redef fun json do
+               var obj = super
+               obj["visibility"] = public_visibility
+               if ini != null then
+                       obj["ini"] = new JsonObject.from(ini.as(not null).to_map)
+               end
+               obj["root"] = to_mentity_ref(root)
+               obj["mgroups"] = to_mentity_refs(mgroups)
+               return obj
+       end
+end
+
+redef class MGroup
+       redef fun json do
+               var obj = super
+               obj["visibility"] = public_visibility
+               obj["is_root"] = is_root
+               obj["mpackage"] = to_mentity_ref(mpackage)
+               obj["default_mmodule"] = to_mentity_ref(default_mmodule)
+               obj["parent"] = to_mentity_ref(parent)
+               obj["mmodules"] = to_mentity_refs(mmodules)
+               obj["mgroups"] = to_mentity_refs(in_nesting.direct_smallers)
+               return obj
+       end
+end
+
+redef class MModule
+       redef fun json do
+               var obj = super
+               obj["location"] = location
+               obj["visibility"] = public_visibility
+               obj["mpackage"] = to_mentity_ref(mpackage)
+               obj["mgroup"] = to_mentity_ref(mgroup)
+               obj["intro_mclasses"] = to_mentity_refs(intro_mclasses)
+               obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+               return obj
+       end
+end
+
+redef class MClass
+       redef fun json do
+               var obj = super
+               obj["visibility"] = visibility
+               var arr = new JsonArray
+               for mparameter in mparameters do arr.add mparameter
+               obj["mparameters"] = arr
+               obj["intro"] = to_mentity_ref(intro)
+               obj["intro_mmodule"] = to_mentity_ref(intro_mmodule)
+               obj["mpackage"] = to_mentity_ref(intro_mmodule.mpackage)
+               obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+               return obj
+       end
+end
+
+redef class MClassDef
+       redef fun json do
+               var obj = super
+               obj["visibility"] = mclass.visibility
+               obj["location"] = location
+               obj["is_intro"] = is_intro
+               var arr = new JsonArray
+               for mparameter in mclass.mparameters do arr.add mparameter
+               obj["mparameters"] = arr
+               obj["mmodule"] = to_mentity_ref(mmodule)
+               obj["mclass"] = to_mentity_ref(mclass)
+               obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+               obj["intro_mproperties"] = to_mentity_refs(intro_mproperties)
+               return obj
+       end
+end
+
+redef class MProperty
+       redef fun json do
+               var obj = super
+               obj["visibility"] = visibility
+               obj["intro"] = to_mentity_ref(intro)
+               obj["intro_mclassdef"] = to_mentity_ref(intro_mclassdef)
+               obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+               return obj
+       end
+end
+
+redef class MMethod
+       redef fun json do
+               var obj = super
+               obj["is_init"] = is_init
+               obj["msignature"] = intro.msignature
+               return obj
+       end
+end
+
+redef class MAttribute
+       redef fun json do
+               var obj = super
+               obj["static_mtype"] = to_mentity_ref(intro.static_mtype)
+               return obj
+       end
+end
+
+redef class MVirtualTypeProp
+       redef fun json do
+               var obj = super
+               obj["mvirtualtype"] = to_mentity_ref(mvirtualtype)
+               obj["bound"] = to_mentity_ref(intro.bound)
+               return obj
+       end
+end
+
+redef class MPropDef
+       redef fun json do
+               var obj = super
+               obj["visibility"] = mproperty.visibility
+               obj["location"] = location
+               obj["is_intro"] = is_intro
+               obj["mclassdef"] = to_mentity_ref(mclassdef)
+               obj["mproperty"] = to_mentity_ref(mproperty)
+               return obj
+       end
+end
+
+redef class MMethodDef
+       redef fun json do
+               var obj = super
+               obj["msignature"] = msignature
+               return obj
+       end
+end
+
+redef class MAttributeDef
+       redef fun json do
+               var obj = super
+               obj["static_mtype"] = to_mentity_ref(static_mtype)
+               return obj
+       end
+end
+
+redef class MVirtualTypeDef
+       redef fun json do
+               var obj = super
+               obj["bound"] = to_mentity_ref(bound)
+               obj["is_fixed"] = is_fixed
+               return obj
+       end
+end
+
+redef class MSignature
+       redef fun json do
+               var obj = new JsonObject
+               obj["arity"] = arity
+               var arr = new JsonArray
+               for mparam in mparameters do arr.add mparam
+               obj["mparams"] = arr
+               obj["return_mtype"] = to_mentity_ref(return_mtype)
+               obj["vararg_rank"] = vararg_rank
+               return obj
+       end
+end
+
+redef class MParameterType
+       redef fun json do
+               var obj = new JsonObject
+               obj["name"] = name
+               obj["rank"] = rank
+               obj["mtype"] = to_mentity_ref(mclass.intro.bound_mtype.arguments[rank])
+               return obj
+       end
+end
+
+redef class MParameter
+       redef fun json do
+               var obj = new JsonObject
+               obj["is_vararg"] = is_vararg
+               obj["name"] = name
+               obj["mtype"] = to_mentity_ref(mtype)
+               return obj
+       end
+end
+
+# Create a ref to a `mentity`.
+fun to_mentity_ref(mentity: nullable MEntity): nullable MEntityRef do
+       if mentity == null then return null
+       return new MEntityRef(mentity)
+end
+
+# Return a collection of `mentities` as a JsonArray of MEntityRefs.
+fun to_mentity_refs(mentities: Collection[MEntity]): JsonArray do
+       var array = new JsonArray
+       for mentity in mentities do array.add to_mentity_ref(mentity)
+       return array
+end
index f130f09..566a5bd 100644 (file)
@@ -132,6 +132,14 @@ class ModelView
                return res
        end
 
+       # Searches the MEntity that matches `full_name`.
+       fun mentity_by_full_name(full_name: String): nullable MEntity do
+               for mentity in mentities do
+                       if mentity.full_name == full_name then return mentity
+               end
+               return null
+       end
+
        # Looks up a MEntity by its full `namespace`.
        #
        # Usefull when `mentities_by_name` returns conflicts.
index 87d3c81..246e813 100644 (file)
@@ -836,9 +836,7 @@ redef class AMethPropdef
                                return
                        end
                else
-                       if mprop.is_broken then
-                               return
-                       end
+                       if mprop.is_broken then return
                        if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, not self isa AMainMethPropdef, mprop) then return
                        check_redef_property_visibility(modelbuilder, self.n_visibility, mprop)
                end
@@ -1197,8 +1195,12 @@ redef class AAttrPropdef
                if mreadprop == null then
                        var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility)
                        mreadprop = new MMethod(mclassdef, readname, self.location, mvisibility)
-                       if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then return
+                       if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then
+                               mreadprop.is_broken = true
+                               return
+                       end
                else
+                       if mreadprop.is_broken then return
                        if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, true, mreadprop) then return
                        check_redef_property_visibility(modelbuilder, self.n_visibility, mreadprop)
                end
@@ -1296,9 +1298,13 @@ redef class AAttrPropdef
                                if mvisibility > protected_visibility then mvisibility = protected_visibility
                        end
                        mwriteprop = new MMethod(mclassdef, writename, self.location, mvisibility)
-                       if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then return
+                       if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then
+                               mwriteprop.is_broken = true
+                               return
+                       end
                        mwriteprop.deprecation = mreadprop.deprecation
                else
+                       if mwriteprop.is_broken then return
                        if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef or else n_kwredef, true, mwriteprop) then return
                        if atwritable != null then
                                check_redef_property_visibility(modelbuilder, atwritable.n_visibility, mwriteprop)
@@ -1608,6 +1614,7 @@ redef class ATypePropdef
                                break
                        end
                else
+                       if mprop.is_broken then return
                        assert mprop isa MVirtualTypeProp
                        check_redef_property_visibility(modelbuilder, self.n_visibility, mprop)
                end
index 9066e0f..5c478f0 100644 (file)
@@ -320,7 +320,7 @@ redef class Catalog
                if errors > 0 then
                        res.add "<li>{errors} errors</li>\n"
                end
-               res.add "<li>{warnings[mpackage]} warnings</li>\n"
+               res.add "<li>{warnings[mpackage]} warnings ({warnings_per_kloc[mpackage]}/kloc)</li>\n"
                res.add "<li>{documentation_score[mpackage]}% documented</li>\n"
                res.add "</ul>\n"
 
@@ -485,6 +485,7 @@ redef class Catalog
                res.add "<th data-field=\"score\" data-sortable=\"true\">score</th>\n"
                res.add "<th data-field=\"errors\" data-sortable=\"true\">errors</th>\n"
                res.add "<th data-field=\"warnings\" data-sortable=\"true\">warnings</th>\n"
+               res.add "<th data-field=\"warnings_per_kloc\" data-sortable=\"true\">w/kloc</th>\n"
                res.add "<th data-field=\"doc\" data-sortable=\"true\">doc</th>\n"
                res.add "</tr></thead>"
                for p in mpackages do
@@ -507,6 +508,7 @@ redef class Catalog
                        res.add "<td>{score[p]}</td>"
                        res.add "<td>{errors[p]}</td>"
                        res.add "<td>{warnings[p]}</td>"
+                       res.add "<td>{warnings_per_kloc[p]}</td>"
                        res.add "<td>{documentation_score[p]}</td>"
                        res.add "</tr>\n"
                end
index cc9ea85..0d89dc6 100644 (file)
@@ -20,7 +20,7 @@ import testing
 
 var toolcontext = new ToolContext
 
-toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show)
+toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show, toolcontext.opt_nitc)
 toolcontext.tooldescription = "Usage: nitunit [OPTION]... <file.nit>...\nExecutes the unit tests from Nit source files."
 
 toolcontext.process_options(args)
@@ -65,6 +65,10 @@ end
 
 "NIT_TESTING".setenv("true")
 
+var test_dir = toolcontext.test_dir
+test_dir.mkdir
+"# This file prevents the Nit modules of the directory to be part of the package".write_to_file(test_dir / "packages.ini")
+
 var page = new HTMLTag("testsuites")
 
 if toolcontext.opt_full.value then mmodules = model.mmodules
index b924ec2..c055ee9 100644 (file)
@@ -47,15 +47,15 @@ private class NitwebPhase
                var host = toolcontext.opt_host.value or else "localhost"
                var port = toolcontext.opt_port.value
 
-               var srv = new NitServer(host, port.to_i)
-               srv.routes.add new Route("/random", new RandomAction(srv, model))
-               srv.routes.add new Route("/doc/:namespace", new DocAction(srv, model, modelbuilder))
-               srv.routes.add new Route("/code/:namespace", new CodeAction(srv, model, modelbuilder))
-               srv.routes.add new Route("/search/:namespace", new SearchAction(srv, model))
-               srv.routes.add new Route("/uml/:namespace", new UMLDiagramAction(srv, model, mainmodule))
-               srv.routes.add new Route("/", new TreeAction(srv, model))
+               var app = new App
+               app.use("/random", new RandomAction(model))
+               app.use("/doc/:namespace", new DocAction(model, modelbuilder))
+               app.use("/code/:namespace", new CodeAction(model, modelbuilder))
+               app.use("/search/:namespace", new SearchAction(model))
+               app.use("/uml/:namespace", new UMLDiagramAction(model, mainmodule))
+               app.use("/", new TreeAction(model))
 
-               srv.listen
+               app.listen(host, port.to_i)
        end
 end
 
index 64ceecf..73ebcd0 100644 (file)
@@ -1,6 +1,6 @@
 Parser and AST for the Nit language
 
-The parser ans the AST are mostly used by all tools.
+The parser and the AST are mostly used by all tools.
 
 The `parser` is the tool that transform source-files into abstract syntax trees (AST) (see `parser_nodes`)
 While the AST is a higher abstraction than blob of text, the AST is still limited and represents only *What the programmer says*.
index 62fbbb0..1d60f15 100644 (file)
@@ -88,8 +88,6 @@ redef class AAnnotation
                        return ""
                else
                        for arg in args do
-                               var format_error = "Syntax Eror: `{name}` expects its arguments to be of type Int or a call to `git_revision`."
-
                                var value
                                value = arg.as_int
                                if value != null then
@@ -107,7 +105,13 @@ redef class AAnnotation
                                        # Get Git short revision
                                        var proc = new ProcessReader("git", "rev-parse", "--short", "HEAD")
                                        proc.wait
-                                       assert proc.status == 0
+                                       if proc.status != 0 then
+                                               # Fallback if this is not a git repository or git bins are missing
+                                               version_fields.add "0"
+                                               modelbuilder.warning(self, "git_revision", "Warning: `git_revision` used outside of a git repository or git binaries not available")
+                                               continue
+                                       end
+
                                        var lines = proc.read_all
                                        var revision = lines.split("\n").first
 
@@ -122,6 +126,7 @@ redef class AAnnotation
                                        continue
                                end
 
+                               var format_error = "Syntax Error: `{name}` expects its arguments to be of type Int or a call to `git_revision`."
                                modelbuilder.error(self, format_error)
                                return ""
                        end
index 62cfb08..4249df5 100644 (file)
@@ -17,6 +17,7 @@ module testing_base
 
 import modelize
 private import parser_util
+import html
 
 redef class ToolContext
        # opt --full
@@ -27,6 +28,8 @@ redef class ToolContext
        var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir")
        # opt --no-act
        var opt_noact = new OptionBool("Does not compile and run tests", "--no-act")
+       # opt --nitc
+       var opt_nitc = new OptionString("nitc compiler to use", "--nitc")
 
        # Working directory for testing.
        fun test_dir: String do
@@ -34,4 +37,137 @@ redef class ToolContext
                if dir == null then return ".nitunit"
                return dir
        end
+
+       # Search the `nitc` compiler to use
+       #
+       # If not `nitc` is suitable, then prints an error and quit.
+       fun find_nitc: String
+       do
+               var nitc = opt_nitc.value
+               if nitc != null then
+                       if not nitc.file_exists then
+                               fatal_error(null, "error: cannot find `{nitc}` given by --nitc.")
+                               abort
+                       end
+                       return nitc
+               end
+
+               nitc = "NITC".environ
+               if nitc != "" then
+                       if not nitc.file_exists then
+                               fatal_error(null, "error: cannot find `{nitc}` given by NITC.")
+                               abort
+                       end
+                       return nitc
+               end
+
+               var nit_dir = nit_dir
+               nitc = nit_dir/"bin/nitc"
+               if not nitc.file_exists then
+                       fatal_error(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.")
+                       abort
+               end
+               return nitc
+       end
+
+       # Execute a system command in a more safe context than `Sys::system`.
+       fun safe_exec(command: String): Int
+       do
+               info(command, 2)
+               var real_command = """
+bash -c "
+ulimit -f {{{ulimit_file}}} 2> /dev/null
+ulimit -t {{{ulimit_usertime}}} 2> /dev/null
+{{{command}}}
+"
+"""
+               return system(real_command)
+       end
+
+       # The maximum size (in KB) of files written by a command executed trough `safe_exec`
+       #
+       # Default: 64MB
+       var ulimit_file = 65536 is writable
+
+       # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec`
+       #
+       # Default: 10 CPU minute
+       var ulimit_usertime = 600 is writable
+end
+
+# A unit test is an elementary test discovered, run and reported bu nitunit.
+#
+# This class factorizes `DocUnit` and `TestCase`.
+abstract class UnitTest
+
+       # Error occurred during test-case execution.
+       var error: nullable String = null is writable
+
+       # Was the test case executed at least once?
+       var was_exec = false is writable
+
+       # Return the `TestCase` in XML format compatible with Jenkins.
+       #
+       # See to_xml
+       fun to_xml: HTMLTag do
+               var tc = new HTMLTag("testcase")
+               tc.attr("classname", xml_classname)
+               tc.attr("name", xml_name)
+               var error = self.error
+               if error != null then
+                       if was_exec then
+                               tc.open("error").append("Runtime Error")
+                       else
+                               tc.open("failure").append("Compilation Error")
+                       end
+                       tc.open("system-err").append(error.trunc(8192).filter_nonprintable)
+               end
+               return tc
+       end
+
+       # The `classname` attribute of the XML format.
+       #
+       # NOTE: jenkins expects a '.' in the classname attr
+       fun xml_classname: String is abstract
+
+       # The `name` attribute of the XML format.
+       #
+       # See to_xml
+       fun xml_name: String is abstract
+end
+
+redef class String
+       # If needed, truncate `self` at `max_length` characters and append an informative `message`.
+       #
+       # ~~~
+       # assert "hello".trunc(10) == "hello"
+       # assert "hello".trunc(2) == "he[truncated. Full size is 5]"
+       # assert "hello".trunc(2, "...") == "he..."
+       # ~~~
+       fun trunc(max_length: Int, message: nullable String): String
+       do
+               if length <= max_length then return self
+               if message == null then message = "[truncated. Full size is {length}]"
+               return substring(0, max_length) + message
+       end
+
+       # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space).
+       #
+       # ~~~
+       # assert "hello".filter_nonprintable == "hello"
+       # assert "\r\n\t".filter_nonprintable == "^13\n^9"
+       # ~~~
+       fun filter_nonprintable: String
+       do
+               var buf = new Buffer
+               for c in self do
+                       var cp = c.code_point
+                       if cp < 32 and c != '\n' then
+                               buf.append "^{cp}"
+                       else
+                               buf.add c
+                       end
+               end
+               return buf.to_s
+       end
 end
index 2514454..1ea2cf7 100644 (file)
@@ -36,12 +36,6 @@ class NitUnitExecutor
        # The XML node associated to the module
        var testsuite: HTMLTag
 
-       # All blocks of code from a same `ADoc`
-       var blocks = new Array[Buffer]
-
-       # All failures from a same `ADoc`
-       var failures = new Array[String]
-
        # Markdown processor used to parse markdown comments and extract code.
        var mdproc = new MarkdownProcessor
 
@@ -55,14 +49,22 @@ class NitUnitExecutor
        # used to generate distinct names
        var cpt = 0
 
+       # The last docunit extracted from a mdoc.
+       #
+       # Is used because a new code-block might just be added to it.
+       var last_docunit: nullable DocUnit = null
+
+       var xml_classname: String is noautoinit
+
+       var xml_name: String is noautoinit
+
        # The entry point for a new `ndoc` node
        # Fill `docunits` with new discovered unit of tests.
-       #
-       # `tc` (testcase) is the pre-filled XML node
-       fun extract(mdoc: MDoc, tc: HTMLTag)
+       fun extract(mdoc: MDoc, xml_classname, xml_name: String)
        do
-               blocks.clear
-               failures.clear
+               last_docunit = null
+               self.xml_classname = xml_classname
+               self.xml_name = xml_name
 
                self.mdoc = mdoc
 
@@ -70,22 +72,6 @@ class NitUnitExecutor
                mdproc.process(mdoc.content.join("\n"))
 
                toolcontext.check_errors
-
-               if not failures.is_empty then
-                       for msg in failures do
-                               var ne = new HTMLTag("failure")
-                               ne.attr("message", msg)
-                               tc.add ne
-                               toolcontext.modelbuilder.unit_entities += 1
-                               toolcontext.modelbuilder.failed_entities += 1
-                       end
-                       if blocks.is_empty then testsuite.add(tc)
-               end
-
-               if blocks.is_empty then return
-               for block in blocks do
-                       docunits.add new DocUnit(mdoc, tc, block.write_to_string)
-               end
        end
 
        # All extracted docunits
@@ -96,6 +82,9 @@ class NitUnitExecutor
        do
                var simple_du = new Array[DocUnit]
                for du in docunits do
+                       # Skip existing errors
+                       if du.error != null then continue
+
                        var ast = toolcontext.parse_something(du.block)
                        if ast isa AExpr then
                                simple_du.add du
@@ -105,6 +94,10 @@ class NitUnitExecutor
                end
 
                test_simple_docunits(simple_du)
+
+               for du in docunits do
+                       testsuite.add du.to_xml
+               end
        end
 
        # Executes multiples doc-units in a shared program.
@@ -128,7 +121,7 @@ class NitUnitExecutor
 
                        i += 1
                        f.write("fun run_{i} do\n")
-                       f.write("# {du.testcase.attrs["name"]}\n")
+                       f.write("# {du.full_name}\n")
                        f.write(du.block)
                        f.write("end\n")
                end
@@ -153,34 +146,19 @@ class NitUnitExecutor
 
                i = 0
                for du in dus do
-                       var tc = du.testcase
-                       toolcontext.modelbuilder.unit_entities += 1
                        i += 1
-                       toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1)
-                       var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 </dev/null")
-
-                       var msg
-                       f = new FileReader.open("{file}.out1")
-                       var n2
-                       n2 = new HTMLTag("system-err")
-                       tc.add n2
-                       msg = f.read_all
-                       f.close
+                       toolcontext.info("Execute doc-unit {du.full_name} in {file} {i}", 1)
+                       var res2 = toolcontext.safe_exec("{file.to_program_name}.bin {i} >'{file}.out1' 2>&1 </dev/null")
 
-                       n2 = new HTMLTag("system-out")
-                       tc.add n2
-                       n2.append(du.block)
+                       var content = "{file}.out1".to_path.read_all
+                       var msg = content.trunc(8192).filter_nonprintable
 
                        if res2 != 0 then
-                               var ne = new HTMLTag("error")
-                               ne.attr("message", msg)
-                               tc.add ne
-                               toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                               du.error = content
+                               toolcontext.warning(du.location, "error", "ERROR: {du.full_name} (in {file}): Runtime error\n{msg}")
                                toolcontext.modelbuilder.failed_entities += 1
                        end
                        toolcontext.check_errors
-
-                       testsuite.add(tc)
                end
        end
 
@@ -188,13 +166,10 @@ class NitUnitExecutor
        # Used for docunits larger than a single block of code (with modules, classes, functions etc.)
        fun test_single_docunit(du: DocUnit)
        do
-               var tc = du.testcase
-               toolcontext.modelbuilder.unit_entities += 1
-
                cpt += 1
                var file = "{prefix}-{cpt}.nit"
 
-               toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
+               toolcontext.info("Execute doc-unit {du.full_name} in {file}", 1)
 
                var f
                f = create_unitfile(file)
@@ -206,38 +181,21 @@ class NitUnitExecutor
                var res = compile_unitfile(file)
                var res2 = 0
                if res == 0 then
-                       res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
+                       res2 = toolcontext.safe_exec("{file.to_program_name}.bin >'{file}.out1' 2>&1 </dev/null")
                end
 
-               var msg
-               f = new FileReader.open("{file}.out1")
-               var n2
-               n2 = new HTMLTag("system-err")
-               tc.add n2
-               msg = f.read_all
-               f.close
-
-               n2 = new HTMLTag("system-out")
-               tc.add n2
-               n2.append(du.block)
-
+               var content = "{file}.out1".to_path.read_all
+               var msg = content.trunc(8192).filter_nonprintable
 
                if res != 0 then
-                       var ne = new HTMLTag("failure")
-                       ne.attr("message", msg)
-                       tc.add ne
-                       toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       du.error = content
+                       toolcontext.warning(du.location, "failure", "FAILURE: {du.full_name} (in {file}):\n{msg}")
                        toolcontext.modelbuilder.failed_entities += 1
                else if res2 != 0 then
-                       var ne = new HTMLTag("error")
-                       ne.attr("message", msg)
-                       tc.add ne
-                       toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       toolcontext.warning(du.location, "error", "ERROR: {du.full_name} (in {file}):\n{msg}")
                        toolcontext.modelbuilder.failed_entities += 1
                end
                toolcontext.check_errors
-
-               testsuite.add(tc)
        end
 
        # Create and fill the header of a unit file `file`.
@@ -268,18 +226,13 @@ class NitUnitExecutor
        # Can terminate the program if the compiler is not found
        private fun compile_unitfile(file: String): Int
        do
-               var nit_dir = toolcontext.nit_dir
-               var nitc = nit_dir/"bin/nitc"
-               if not nitc.file_exists then
-                       toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
+               var nitc = toolcontext.find_nitc
                var opts = new Array[String]
                if mmodule != null then
                        opts.add "-I {mmodule.filepath.dirname}"
                end
                var cmd = "{nitc} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
-               var res = sys.system(cmd)
+               var res = toolcontext.safe_exec(cmd)
                return res
        end
 end
@@ -335,31 +288,122 @@ private class NitunitDecorator
                        end
 
                        executor.toolcontext.warning(location, "invalid-block", "{message} To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).")
-                       executor.failures.add("{location}: {message}")
+                       executor.toolcontext.modelbuilder.failed_entities += 1
+
+                       var du = new_docunit
+                       du.block += code
+                       du.error = "{location}: {message}"
                        return
                end
 
                # Create a first block
                # Or create a new block for modules that are more than a main part
-               if executor.blocks.is_empty or ast isa AModule then
-                       executor.blocks.add(new Buffer)
+               var last_docunit = executor.last_docunit
+               if last_docunit == null or ast isa AModule then
+                       last_docunit = new_docunit
+                       executor.last_docunit = last_docunit
                end
 
                # Add it to the file
-               executor.blocks.last.append code
+               last_docunit.block += code
+
+               # In order to retrieve precise positions,
+               # the real position of each line of the raw_content is stored.
+               # See `DocUnit::real_location`
+               line_offset -= loc.line_start - 1
+               for i in [loc.line_start..loc.line_end] do
+                       last_docunit.lines.add i + line_offset
+                       last_docunit.columns.add column_offset
+               end
+       end
+
+       # Return and register a new empty docunit
+       fun new_docunit: DocUnit
+       do
+               var mdoc = executor.mdoc
+               assert mdoc != null
+
+               var next_number = 0
+               var name = executor.xml_name
+               if executor.docunits.not_empty and executor.docunits.last.mdoc == mdoc then
+                       next_number = executor.docunits.last.number + 1
+                       name += "+" + next_number.to_s
+               end
+
+               var res = new DocUnit(mdoc, next_number, "", executor.xml_classname, name)
+               executor.docunits.add res
+               executor.toolcontext.modelbuilder.unit_entities += 1
+               return res
        end
 end
 
-# A unit-test to run
+# A unit-test extracted from some documentation.
+#
+# A docunit is extracted from the code-blocks of mdocs.
+# Each mdoc can contains more than one docunit, and a single docunit can be made of more that a single code-block.
 class DocUnit
+       super UnitTest
+
        # The doc that contains self
        var mdoc: MDoc
 
-       # The XML node that contains the information about the execution
-       var testcase: HTMLTag
+       # The numbering of self in mdoc (starting with 0)
+       var number: Int
+
+       # The name of the unit to show in messages
+       fun full_name: String do
+               var mentity = mdoc.original_mentity
+               if mentity != null then return mentity.full_name
+               return xml_classname + "." + xml_name
+       end
 
-       # The text of the code to execute
+       # The text of the code to execute.
+       #
+       # This is the verbatim content on one, or more, code-blocks from `mdoc`
        var block: String
+
+       # For each line in `block`, the associated line in the mdoc
+       #
+       # Is used to give precise locations
+       var lines = new Array[Int]
+
+       # For each line in `block`, the associated column in the mdoc
+       #
+       # Is used to give precise locations
+       var columns = new Array[Int]
+
+       # The location of the whole docunit.
+       #
+       # If `self` is made of multiple code-blocks, then the location
+       # starts at the first code-books and finish at the last one, thus includes anything between.
+       var location: Location is lazy do
+               return new Location(mdoc.location.file, lines.first, lines.last+1, columns.first+1, 0)
+       end
+
+       # Compute the real location of a node on the `ast` based on `mdoc.location`
+       #
+       # The result is basically: ast_location + markdown location of the piece + mdoc.location
+       #
+       # The fun is that a single docunit can be made of various pieces of code blocks.
+       fun real_location(ast_location: Location): Location
+       do
+               var mdoc = self.mdoc
+               var res = new Location(mdoc.location.file, lines[ast_location.line_start-1],
+                       lines[ast_location.line_end-1],
+                       columns[ast_location.line_start-1] + ast_location.column_start,
+                       columns[ast_location.line_end-1] + ast_location.column_end)
+               return res
+       end
+
+       redef fun to_xml
+       do
+               var res = super
+               res.open("system-out").append(block)
+               return res
+       end
+
+       redef var xml_classname
+       redef var xml_name
 end
 
 redef class ModelBuilder
@@ -400,8 +444,6 @@ redef class ModelBuilder
                prefix = prefix.join_path(mmodule.to_s)
                var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
 
-               var tc
-
                do
                        total_entities += 1
                        var nmoduledecl = nmodule.n_moduledecl
@@ -409,11 +451,8 @@ redef class ModelBuilder
                        var ndoc = nmoduledecl.n_doc
                        if ndoc == null then break label x
                        doc_entities += 1
-                       tc = new HTMLTag("testcase")
                        # NOTE: jenkins expects a '.' in the classname attr
-                       tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
-                       tc.attr("name", "<module>")
-                       d2m.extract(ndoc.to_mdoc, tc)
+                       d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + ".<module>", "<module>")
                end label x
                for nclassdef in nmodule.n_classdefs do
                        var mclassdef = nclassdef.mclassdef
@@ -423,10 +462,7 @@ redef class ModelBuilder
                                var ndoc = nclassdef.n_doc
                                if ndoc != null then
                                        doc_entities += 1
-                                       tc = new HTMLTag("testcase")
-                                       tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
-                                       tc.attr("name", "<class>")
-                                       d2m.extract(ndoc.to_mdoc, tc)
+                                       d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, "<class>")
                                end
                        end
                        for npropdef in nclassdef.n_propdefs do
@@ -436,10 +472,7 @@ redef class ModelBuilder
                                var ndoc = npropdef.n_doc
                                if ndoc != null then
                                        doc_entities += 1
-                                       tc = new HTMLTag("testcase")
-                                       tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
-                                       tc.attr("name", mpropdef.mproperty.full_name)
-                                       d2m.extract(ndoc.to_mdoc, tc)
+                                       d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, mpropdef.mproperty.full_name)
                                end
                        end
                end
@@ -465,18 +498,13 @@ redef class ModelBuilder
                prefix = prefix.join_path(mgroup.to_s)
                var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
 
-               var tc
-
                total_entities += 1
                var mdoc = mgroup.mdoc
                if mdoc == null then return ts
 
                doc_entities += 1
-               tc = new HTMLTag("testcase")
                # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit." + mgroup.full_name)
-               tc.attr("name", "<group>")
-               d2m.extract(mdoc, tc)
+               d2m.extract(mdoc, "nitunit." + mgroup.full_name, "<group>")
 
                d2m.run_tests
 
@@ -496,17 +524,11 @@ redef class ModelBuilder
                var prefix = toolcontext.test_dir / "file"
                var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
 
-               var tc
-
                total_entities += 1
                doc_entities += 1
 
-               tc = new HTMLTag("testcase")
                # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit.<file>")
-               tc.attr("name", file)
-
-               d2m.extract(mdoc, tc)
+               d2m.extract(mdoc, "nitunit.<file>", file)
                d2m.run_tests
 
                return ts
index 0de6e0a..ee2701f 100644 (file)
@@ -183,12 +183,7 @@ class TestSuite
        # Compile all `test_cases` cases in one file.
        fun compile do
                # find nitc
-               var nit_dir = toolcontext.nit_dir
-               var nitc = nit_dir/"bin/nitc"
-               if not nitc.file_exists then
-                       toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
+               var nitc = toolcontext.find_nitc
                # compile test suite
                var file = test_file
                var module_file = mmodule.location.file
@@ -199,7 +194,7 @@ class TestSuite
                end
                var include_dir = module_file.filename.dirname
                var cmd = "{nitc} --no-color '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
-               var res = sys.system(cmd)
+               var res = toolcontext.safe_exec(cmd)
                var f = new FileReader.open("{file}.out")
                var msg = f.read_all
                f.close
@@ -219,6 +214,7 @@ end
 
 # A test case is a unit test considering only a `MMethodDef`.
 class TestCase
+       super UnitTest
 
        # Test suite wich `self` belongs to.
        var test_suite: TestSuite
@@ -253,7 +249,7 @@ class TestCase
                var method_name = test_method.name
                var test_file = test_suite.test_file
                var res_name = "{test_file}_{method_name.escape_to_c}"
-               var res = sys.system("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
+               var res = toolcontext.safe_exec("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
                var f = new FileReader.open("{res_name}.out1")
                var msg = f.read_all
                f.close
@@ -264,36 +260,36 @@ class TestCase
                        toolcontext.warning(loc, "failure",
                           "ERROR: {method_name} (in file {test_file}.nit): {msg}")
                        toolcontext.modelbuilder.failed_tests += 1
+               else
+                       var mmodule = test_method.mclassdef.mmodule
+                       var file = mmodule.filepath
+                       if file != null then
+                               var sav = file.dirname / mmodule.name + ".sav" / test_method.name + ".res"
+                               if sav.file_exists then
+                                       toolcontext.info("Diff output with {sav}", 1)
+                                       res = toolcontext.safe_exec("diff -u --label 'expected:{sav}' --label 'got:{res_name}.out1' '{sav}' '{res_name}.out1' > '{res_name}.diff' 2>&1 </dev/null")
+                                       if res != 0 then
+                                               msg = "Diff\n" + "{res_name}.diff".to_path.read_all
+                                               error = msg
+                                               toolcontext.warning(loc, "failure",
+                                               "ERROR: {method_name} (in file {test_file}.nit): {msg}")
+                                               toolcontext.modelbuilder.failed_tests += 1
+                                       end
+                               else
+                                       toolcontext.info("No diff: {sav} not found", 2)
+                               end
+                       end
                end
                toolcontext.check_errors
        end
 
-       # Error occured during test-case execution.
-       var error: nullable String = null
-
-       # Was the test case executed at least one?
-       var was_exec = false
-
-       # Return the `TestCase` in XML format compatible with Jenkins.
-       fun to_xml: HTMLTag do
+       redef fun xml_classname do
                var mclassdef = test_method.mclassdef
-               var tc = new HTMLTag("testcase")
-               # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name)
-               tc.attr("name", test_method.mproperty.full_name)
-               if was_exec then
-                       tc.add  new HTMLTag("system-err")
-                       var n = new HTMLTag("system-out")
-                       n.append "out"
-                       tc.add n
-                       var error = self.error
-                       if error != null then
-                               n = new HTMLTag("error")
-                               n.attr("message", error.to_s)
-                               tc.add n
-                       end
-               end
-               return tc
+               return "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name
+       end
+
+       redef fun xml_name do
+               return test_method.mproperty.full_name
        end
 end
 
index 2b49b9e..0310a9c 100644 (file)
@@ -32,12 +32,12 @@ redef class MEntity
        # * MPropdef: `foo(e)`
        var html_name: String is lazy do return name.html_escape
 
-       # MEntity namespace escaped for html.
-       fun html_raw_namespace: String is abstract
+       # Returns the MEntity full_name escaped for html.
+       var html_full_name: String is lazy do return full_name.html_escape
 
        # Link to MEntity in the web server.
        # TODO this should be parameterizable... but how?
-       fun html_link: Link do return new Link("/doc/{html_raw_namespace}", html_name)
+       fun html_link: Link do return new Link("/doc/{full_name}", html_name)
 
        # Returns the list of keyword used in `self` declaration.
        fun html_modifiers: Array[String] is abstract
@@ -107,22 +107,12 @@ redef class MEntity
 end
 
 redef class MPackage
-       redef fun html_raw_namespace do return html_name
-
        redef var html_modifiers = ["package"]
        redef fun html_namespace do return html_link
        redef var css_classes = ["public"]
 end
 
 redef class MGroup
-       redef fun html_raw_namespace do
-               var parent = self.parent
-               if parent != null then
-                       return "{parent.html_raw_namespace}::{html_name}"
-               end
-               return "{mpackage.html_raw_namespace}::{html_name}"
-       end
-
        redef var html_modifiers = ["group"]
 
        # Depends if `self` is root or not.
@@ -161,17 +151,6 @@ redef class MModule
                return tpl
        end
 
-       redef fun html_raw_namespace do
-               var mpackage = self.mpackage
-               var mgroup = self.mgroup
-               if mgroup != null then
-                       return "{mgroup.html_raw_namespace}::{html_name}"
-               else if mpackage != null then
-                       return "{mpackage.html_raw_namespace}::{html_name}"
-               end
-               return html_name
-       end
-
        redef var css_classes = ["public"]
 end
 
@@ -211,8 +190,6 @@ redef class MClass
                return tpl
        end
 
-       redef fun html_raw_namespace do return intro.html_raw_namespace
-
        # Returns `intro.html_short_signature`.
        fun html_short_signature: Template do return intro.html_short_signature
 
@@ -224,8 +201,6 @@ redef class MClass
 end
 
 redef class MClassDef
-       redef fun html_raw_namespace do return "{mmodule.html_raw_namespace}::{html_name}"
-
        redef fun mdoc_or_fallback do return mdoc or else mclass.mdoc_or_fallback
 
        # Depends if `self` is an intro or not.
@@ -342,8 +317,6 @@ redef class MProperty
                return tpl
        end
 
-       redef fun html_raw_namespace do return intro.html_raw_namespace
-
        # Returns `intro.html_short_signature`.
        fun html_short_signature: Template do return intro.html_short_signature
 
@@ -354,7 +327,6 @@ redef class MProperty
 end
 
 redef class MPropDef
-       redef fun html_raw_namespace do return "{mclassdef.html_raw_namespace}::{html_name}"
        redef fun mdoc_or_fallback do return mdoc or else mproperty.mdoc_or_fallback
 
        # Depends if `self` is an intro or not.
@@ -601,12 +573,10 @@ end
 redef class MParameterType
        redef fun html_short_signature do return html_link
        redef fun html_signature do return html_link
-       redef fun html_raw_namespace do return html_name
 end
 
 redef class MVirtualType
        redef fun html_signature do return html_link
-       redef fun html_raw_namespace do return html_name
 end
 
 redef class MSignature
index f4c4584..e9198ae 100644 (file)
@@ -22,10 +22,10 @@ import uml
 class TreeAction
        super ModelAction
 
-       redef fun answer(request, url) do
-               var model = init_model_view(request)
+       redef fun get(req, res) do
+               var model = init_model_view(req)
                var view = new HtmlHomePage(model.to_tree)
-               return render_view(view)
+               res.send_view(view)
        end
 end
 
@@ -34,22 +34,20 @@ class SearchAction
        super ModelAction
 
        # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
+       redef fun get(req, res) do
+               var namespace = req.param("namespace")
+               var model = init_model_view(req)
+               var mentity = find_mentity(model, namespace)
+               if mentity == null then
+                       res.error(404)
+                       return
                end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if request.is_json_asked then
-                       var json = new JsonArray
-                       for mentity in mentities do
-                               json.add mentity.to_json
-                       end
-                       return render_json(json)
+               if req.is_json_asked then
+                       res.json(mentity.to_json)
+                       return
                end
-               var view = new HtmlResultPage(namespace, mentities)
-               return render_view(view)
+               var view = new HtmlResultPage(namespace or else "null", [mentity])
+               res.send_view(view)
        end
 end
 
@@ -60,19 +58,16 @@ class CodeAction
        # Modelbuilder used to access sources.
        var modelbuilder: ModelBuilder
 
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
-               end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if mentities.is_empty then
-                       return render_error(404, "No mentity matching this namespace.")
+       redef fun get(req, res) do
+               var namespace = req.param("namespace")
+               var model = init_model_view(req)
+               var mentity = find_mentity(model, namespace)
+               if mentity == null then
+                       res.error(404)
+                       return
                end
-               var view = new HtmlSourcePage(modelbuilder, mentities.first)
-               return render_view(view)
+               var view = new HtmlSourcePage(modelbuilder, mentity)
+               res.send_view(view)
        end
 end
 
@@ -83,19 +78,21 @@ class DocAction
        # Modelbuilder used to access sources.
        var modelbuilder: ModelBuilder
 
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
+       redef fun get(req, res) do
+               var namespace = req.param("namespace")
+               var model = init_model_view(req)
+               var mentity = find_mentity(model, namespace)
+               if mentity == null then
+                       res.error(404)
+                       return
                end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if mentities.is_empty then
-                       return render_error(404, "No mentity matching this namespace.")
+               if req.is_json_asked then
+                       res.json(mentity.to_json)
+                       return
                end
-               var view = new HtmlDocPage(modelbuilder, mentities.first)
-               return render_view(view)
+
+               var view = new HtmlDocPage(modelbuilder, mentity)
+               res.send_view(view)
        end
 end
 
@@ -106,20 +103,17 @@ class UMLDiagramAction
        # Mainmodule used for hierarchy flattening.
        var mainmodule: MModule
 
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
+       redef fun get(req, res) do
+               var namespace = req.param("namespace")
+               var model = init_model_view(req)
+               var mentity = find_mentity(model, namespace)
+               if mentity == null then
+                       res.error(404)
+                       return
                end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if mentities.is_empty then
-                       return render_error(404, "No mentity matching this namespace.")
-               end
-               var mentity = mentities.first
-               if mentity isa MClassDef then mentity = mentity.mclass
 
                var dot
+               if mentity isa MClassDef then mentity = mentity.mclass
                if mentity isa MClass then
                        var uml = new UMLModel(model, mainmodule)
                        dot = uml.generate_class_uml.write_to_string
@@ -127,10 +121,11 @@ class UMLDiagramAction
                        var uml = new UMLModel(model, mentity)
                        dot = uml.generate_package_uml.write_to_string
                else
-                       return render_error(404, "No diagram matching this namespace.")
+                       res.error(404)
+                       return
                end
-               var view = new HtmlDotPage(dot, mentity.html_name)
-               return render_view(view)
+               var view = new HtmlDotPage(dot, mentity.as(not null).html_name)
+               res.send_view(view)
        end
 end
 
@@ -138,11 +133,10 @@ end
 class RandomAction
        super ModelAction
 
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var n = request.int_arg("n") or else 10
-               var k = request.string_arg("k") or else "modules"
-               var model = init_model_view(request)
+       redef fun get(req, res) do
+               var n = req.int_arg("n") or else 10
+               var k = req.string_arg("k") or else "modules"
+               var model = init_model_view(req)
                var mentities: Array[MEntity]
                if k == "modules" then
                        mentities = model.mmodules.to_a
@@ -153,30 +147,15 @@ class RandomAction
                end
                mentities.shuffle
                mentities = mentities.sub(0, n)
-               if request.is_json_asked then
+               if req.is_json_asked then
                        var json = new JsonArray
                        for mentity in mentities do
                                json.add mentity.to_json
                        end
-                       return render_json(json)
+                       res.json(json)
+                       return
                end
                var view = new HtmlResultPage("random", mentities)
-               return render_view(view)
-       end
-end
-
-redef class MEntity
-
-       # Return `self` as a JsonObject.
-       fun to_json: JsonObject do
-               var obj = new JsonObject
-               obj["name"] = html_name
-               obj["namespace"] = html_raw_namespace
-               var mdoc = self.mdoc
-               if mdoc != null then
-                       obj["synopsis"] = mdoc.content.first.html_escape
-                       obj["mdoc"] = mdoc.content.join("\n").html_escape
-               end
-               return obj
+               res.send_view(view)
        end
 end
index ecf639e..258ec60 100644 (file)
 module web_base
 
 import model::model_views
-import nitcorn
-import json
-
-# Nitcorn server runned by `nitweb`.
-#
-# Usage:
-#
-# ~~~nitish
-# var srv = new NitServer("localhost", 3000)
-# srv.routes.add new Route("/", new MyAction)
-# src.listen
-# ~~~
-class NitServer
-
-       # Host to bind.
-       var host: String
-
-       # Port to use.
-       var port: Int
-
-       # Routes knwon by the server.
-       var routes = new Array[Route]
-
-       # Start listen on `host:port`.
-       fun listen do
-               var iface = "{host}:{port}"
-               print "Launching server on http://{iface}/"
-
-               var vh = new VirtualHost(iface)
-               for route in routes do vh.routes.add route
-
-               var fac = new HttpFactory.and_libevent
-               fac.config.virtual_hosts.add vh
-               fac.run
-       end
-end
-
-# Specific nitcorn Action for nitweb.
-class NitAction
-       super Action
-
-       # Link to the NitServer that runs this action.
-       var srv: NitServer
-
-       # Build a custom http response for errors.
-       fun render_error(code: Int, message: String): HttpResponse do
-               var response = new HttpResponse(code)
-               var tpl = new Template
-               tpl.add "<h1>Error {code}</h1>"
-               tpl.add "<pre><code>{message.html_escape}</code></pre>"
-               response.body = tpl.write_to_string
-               return response
-       end
-
-       # Render a view as a HttpResponse 200.
-       fun render_view(view: NitView): HttpResponse do
-               var response = new HttpResponse(200)
-               response.body = view.render(srv).write_to_string
-               return response
-       end
-
-       # Return a HttpResponse containing `json`.
-       fun render_json(json: Jsonable): HttpResponse do
-               var response = new HttpResponse(200)
-               response.body = json.to_json
-               return response
-       end
-end
+import model::model_json
+import popcorn
 
 # Specific nitcorn Action that uses a Model
 class ModelAction
-       super NitAction
+       super Handler
 
        # Model to use.
        var model: Model
 
+       # Find the MEntity ` with `full_name`.
+       fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
+               if full_name == null then return null
+               return model.mentity_by_full_name(full_name.from_percent_encoding)
+       end
+
        # Init the model view from the `req` uri parameters.
        fun init_model_view(req: HttpRequest): ModelView do
                var view = new ModelView(model)
-
                var show_private = req.bool_arg("private") or else false
                if not show_private then view.min_visibility = protected_visibility
 
@@ -111,7 +50,12 @@ end
 # A NitView is rendered by an action.
 interface NitView
        # Renders this view and returns something that can be written to a HTTP response.
-       fun render(srv: NitServer): Writable is abstract
+       fun render: Writable is abstract
+end
+
+redef class HttpResponse
+       # Render a NitView as response.
+       fun send_view(view: NitView, status: nullable Int) do send(view.render, status)
 end
 
 redef class HttpRequest
index 950c839..58016e2 100644 (file)
@@ -27,7 +27,7 @@ class HtmlHomePage
        # Loaded model to display.
        var tree: MEntityTree
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, "Loaded model")
                tpl.add tree.html_list
@@ -45,7 +45,7 @@ class HtmlResultPage
        # Result set
        var results: Array[MEntity]
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, "Results for {query}")
                if results.is_empty then
@@ -55,7 +55,7 @@ class HtmlResultPage
                var list = new UnorderedList
                for mentity in results do
                        var link = mentity.html_link
-                       link.text = mentity.html_raw_namespace
+                       link.text = mentity.html_full_name
                        list.add_li new ListItem(link)
                end
                tpl.add list
@@ -76,7 +76,7 @@ class HtmlSourcePage
        # HiglightVisitor used to hilight the source code
        var hl = new HighlightVisitor
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, "Source Code")
                tpl.add render_source
@@ -103,7 +103,7 @@ end
 class HtmlDocPage
        super HtmlSourcePage
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, mentity.html_name)
                tpl.add "<p>"
@@ -130,7 +130,7 @@ class HtmlDotPage
        # Page title.
        var title: String
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, title)
                tpl.add render_dot
diff --git a/tests/base_redef.nit b/tests/base_redef.nit
new file mode 100644 (file)
index 0000000..d42bfec
--- /dev/null
@@ -0,0 +1,65 @@
+# 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 core::kernel
+
+class A
+       fun f do 1.output
+       fun f2: Int do return 2
+       fun f2=(i:Int) do i.output
+       fun f3=(i:Int) do i.output
+       var v = 4
+       type T: A
+       fun t(t: T): T do return t
+end
+
+class B
+       super A#alt1#
+       redef fun f do 10.output
+       redef var f2 = 20
+       var f3 = 30 is redef writable
+       redef var v = 40
+       redef type T: B
+end
+
+class C
+       super B#alt2#
+       redef fun f do 100.output
+       redef var f2 = 200
+       redef var f3 = 300
+       redef var v = 400
+       redef type T: C
+
+end
+
+var a = new A
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
+
+a = new B
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
+
+a = new C
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
index 30c792b..70e648a 100755 (executable)
@@ -1,5 +1,5 @@
 #!/bin/sh
-printf "%s\n" "$@" \
+ls -1 -- "%s\n" "$@" \
        ../src/nit*.nit \
        ../src/test_*.nit \
        ../src/examples/*.nit \
@@ -9,11 +9,11 @@ printf "%s\n" "$@" \
        ../examples/*/src/*_android.nit \
        ../examples/*/src/*_linux.nit \
        ../examples/*/src/*_null.nit \
-       ../examples/nitcorn/src/*.nit \
        ../lib/*/examples/*.nit \
        ../lib/*/examples/*/*.nit \
        ../contrib/friendz/src/solver_cmd.nit \
        ../contrib/neo_doxygen/src/tests/neo_doxygen_*.nit \
        ../contrib/pep8analysis/src/pep8analysis.nit \
        ../contrib/nitiwiki/src/nitiwiki.nit \
-       *.nit
+       *.nit \
+       | grep -v ../lib/popcorn/examples/
diff --git a/tests/sav/base_redef.res b/tests/sav/base_redef.res
new file mode 100644 (file)
index 0000000..3b792d6
--- /dev/null
@@ -0,0 +1,11 @@
+1
+-2
+2
+-3
+1
+10
+-2
+10
+100
+-2
+100
diff --git a/tests/sav/base_redef_alt1.res b/tests/sav/base_redef_alt1.res
new file mode 100644 (file)
index 0000000..50e360c
--- /dev/null
@@ -0,0 +1,5 @@
+alt/base_redef_alt1.nit:29,12: Error: no property `B::f` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:30,12--13: Error: no property `B::f2` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:31,6--7: Error: no property `B::f3=` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:32,12: Error: no property `B::v` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:33,2--16: Error: no property `B::T` is inherited. Remove the `redef` keyword to define a new property.
diff --git a/tests/sav/base_redef_alt2.res b/tests/sav/base_redef_alt2.res
new file mode 100644 (file)
index 0000000..1c263c4
--- /dev/null
@@ -0,0 +1,5 @@
+alt/base_redef_alt2.nit:38,12: Error: no property `C::f` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:39,12--13: Error: no property `C::f2` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:40,12--13: Error: no property `C::f3` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:41,12: Error: no property `C::v` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:42,2--16: Error: no property `C::T` is inherited. Remove the `redef` keyword to define a new property.
index 95abffa..19b015f 100644 (file)
@@ -75,7 +75,7 @@
 </ul>
 <h3>Quality</h3>
 <ul class="box">
-<li>28 warnings</li>
+<li>28 warnings (63/kloc)</li>
 <li>93% documented</li>
 </ul>
 <h3>Tags</h3>
index 58aa2a8..48642c1 100644 (file)
@@ -1,6 +1,8 @@
-test_nitunit.nit:20,1--22,0: ERROR: nitunit.test_nitunit::test_nitunit.test_nitunit::X.<class> (in .nitunit/test_nitunit-2.nit): Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5)
+test_nitunit.nit:21,7--22,0: ERROR: test_nitunit$X (in .nitunit/test_nitunit-2.nit):
+Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5)
 
-test_nitunit.nit:23,2--25,0: FAILURE: nitunit.test_nitunit::test_nitunit.test_nitunit::X.test_nitunit::X::foo (in .nitunit/test_nitunit-3.nit): .nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
+test_nitunit.nit:24,8--25,0: FAILURE: test_nitunit$X$foo (in .nitunit/test_nitunit-3.nit):
+.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
 
 test_test_nitunit.nit:36,2--40,4: ERROR: test_foo1 (in file .nitunit/gen_test_test_nitunit.nit): Runtime error: Assert failed (test_test_nitunit.nit:39)
 
@@ -9,10 +11,9 @@ Entities: 27; Documented ones: 3; With nitunits: 3; Failures: 2
 
 TestSuites:
 Class suites: 1; Test Cases: 3; Failures: 1
-<testsuites><testsuite package="test_nitunit::test_nitunit"><testcase classname="nitunit.test_nitunit::test_nitunit.&lt;module&gt;" name="&lt;module&gt;"><system-err></system-err><system-out>assert true
-</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="&lt;class&gt;"><system-err></system-err><system-out>assert false
-</system-out><error message="Runtime error: Assert failed (.nitunit&#47;test_nitunit-2.nit:5)
-"></error></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo"><system-err></system-err><system-out>assert undefined_identifier
-</system-out><failure message=".nitunit&#47;test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
-"></failure></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo"><system-err></system-err><system-out>out</system-out></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><system-err></system-err><system-out>out</system-out><error message="Runtime error: Assert failed (test_test_nitunit.nit:39)
-"></error></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"><system-err></system-err><system-out>out</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit::test_nitunit"><testcase classname="nitunit.test_nitunit::test_nitunit.&lt;module&gt;" name="&lt;module&gt;"><system-out>assert true
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="&lt;class&gt;"><system-out>assert false
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo"><failure>Compilation Error</failure><system-err>.nitunit&#47;test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
+</system-err><system-out>assert undefined_identifier
+</system-out></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo"></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><error>Runtime Error</error><system-err>Runtime error: Assert failed (test_test_nitunit.nit:39)
+</system-err></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"></testcase></testsuite></testsuites>
\ No newline at end of file
index 4f4ab53..4cb3aaa 100644 (file)
@@ -5,17 +5,17 @@ Entities: 4; Documented ones: 3; With nitunits: 3; Failures: 0
 TestSuites:
 No test cases found
 Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit2::test_nitunit2"><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo1"><system-err></system-err><system-out>if true then
+<testsuites><testsuite package="test_nitunit2::test_nitunit2"><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo1"><system-out>if true then
 
    assert true
 
 end
-</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::bar2"><system-err></system-err><system-out>if true then
+</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::bar2"><system-out>if true then
 
     assert true
 
 end
-</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo3"><system-err></system-err><system-out>var a = 1
+</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo3"><system-out>var a = 1
 assert a == 1
 assert a == 1
 </system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index dd4f11a..8fa1c3c 100644 (file)
@@ -5,7 +5,7 @@ Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 0
 TestSuites:
 No test cases found
 Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_doc2::test_doc2"><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo1"><system-err></system-err><system-out>assert true # tested
-</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo2"><system-err></system-err><system-out>assert true # tested
-</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo3"><system-err></system-err><system-out>assert true # tested
+<testsuites><testsuite package="test_doc2::test_doc2"><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo1"><system-out>assert true # tested
+</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo2"><system-out>assert true # tested
+</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo3"><system-out>assert true # tested
 </system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index 34d34af..ca7ee6a 100644 (file)
@@ -1,5 +1,6 @@
 test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\]. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).
-test_nitunit3/README.md:1,0--13,0: ERROR: nitunit.test_nitunit3>.<group> (in .nitunit/test_nitunit3-0.nit): Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
+test_nitunit3/README.md:4,2--15,0: ERROR: test_nitunit3> (in .nitunit/test_nitunit3-0.nit): Runtime error
+Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
 
 DocUnits:
 Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2
@@ -7,8 +8,9 @@ Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2
 TestSuites:
 No test cases found
 Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit3&gt;"><testcase classname="nitunit.test_nitunit3&gt;" name="&lt;group&gt;"><failure message="test_nitunit3&#47;README.md:7,3--5: Syntax Error: unexpected malformed character &#39;\]."></failure><system-err></system-err><system-out>assert false
+<testsuites><testsuite package="test_nitunit3&gt;"><testcase classname="nitunit.test_nitunit3&gt;" name="&lt;group&gt;"><failure>Compilation Error</failure><system-err>Runtime error: Assert failed (.nitunit&#47;test_nitunit3-0.nit:7)
+</system-err><system-out>assert false
 assert true
-</system-out><error message="Runtime error: Assert failed (.nitunit&#47;test_nitunit3-0.nit:7)
-"></error></testcase></testsuite><testsuite package="test_nitunit3::test_nitunit3"><testcase classname="nitunit.test_nitunit3::test_nitunit3.&lt;module&gt;" name="&lt;module&gt;"><system-err></system-err><system-out>assert true
+</system-out></testcase><testcase classname="nitunit.test_nitunit3&gt;" name="&lt;group&gt;+1"><failure>Compilation Error</failure><system-err>test_nitunit3&#47;README.md:7,3--5: Syntax Error: unexpected malformed character &#39;\].</system-err><system-out>;&#39;\][]
+</system-out></testcase></testsuite><testsuite package="test_nitunit3::test_nitunit3"><testcase classname="nitunit.test_nitunit3::test_nitunit3.&lt;module&gt;" name="&lt;module&gt;"><system-out>assert true
 </system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index fc20a76..0888b04 100644 (file)
@@ -1,4 +1,5 @@
-test_nitunit_md.md:1,0--15,0: ERROR: nitunit.<file>.test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error: Assert failed (.nitunit/file-0.nit:8)
+test_nitunit_md.md:4,2--16,0: ERROR: nitunit.<file>.test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error
+Runtime error: Assert failed (.nitunit/file-0.nit:8)
 
 DocUnits:
 Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1
@@ -6,8 +7,8 @@ Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1
 TestSuites:
 No test cases found
 Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit_md.md:1,0--15,0"><testcase classname="nitunit.&lt;file&gt;" name="test_nitunit_md.md:1,0--15,0"><system-err></system-err><system-out>var a = 1
+<testsuites><testsuite package="test_nitunit_md.md:1,0--15,0"><testcase classname="nitunit.&lt;file&gt;" name="test_nitunit_md.md:1,0--15,0"><failure>Compilation Error</failure><system-err>Runtime error: Assert failed (.nitunit&#47;file-0.nit:8)
+</system-err><system-out>var a = 1
 assert 1 == 1
 assert false
-</system-out><error message="Runtime error: Assert failed (.nitunit&#47;file-0.nit:8)
-"></error></testcase></testsuite></testsuites>
\ No newline at end of file
+</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
index eb25a0d..afa1ac3 100644 (file)
@@ -7,4 +7,7 @@ Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 3
 TestSuites:
 No test cases found
 Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure message="test_doc3.nit:17,9--15: Syntax Error: unexpected identifier &#39;garbage&#39;."></failure></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure message="test_doc3.nit:23,4--10: Syntax Error: unexpected identifier &#39;garbage&#39;."></failure></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure message="test_doc3.nit:30,4--10: Syntax Error: unexpected identifier &#39;garbage&#39;."></failure></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure>Compilation Error</failure><system-err>test_doc3.nit:17,9--15: Syntax Error: unexpected identifier &#39;garbage&#39;.</system-err><system-out> *garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure>Compilation Error</failure><system-err>test_doc3.nit:23,4--10: Syntax Error: unexpected identifier &#39;garbage&#39;.</system-err><system-out>*garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure>Compilation Error</failure><system-err>test_doc3.nit:30,4--10: Syntax Error: unexpected identifier &#39;garbage&#39;.</system-err><system-out>*garbage*
+</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index de8f951..27d74a9 100644 (file)
@@ -1,16 +1,33 @@
-test_nitunit4/test_nitunit4.nit:22,2--25,4: ERROR: test_foo (in file .nitunit/gen_test_nitunit4.nit): Before Test
+test_nitunit4/test_nitunit4.nit:22,2--26,4: ERROR: test_foo (in file .nitunit/gen_test_nitunit4.nit): Before Test
 Tested method
 After Test
 Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31)
 
+test_nitunit4/test_nitunit4.nit:32,2--34,4: ERROR: test_baz (in file .nitunit/gen_test_nitunit4.nit): Diff
+--- expected:test_nitunit4/test_nitunit4.sav/test_baz.res
++++ got:.nitunit/gen_test_nitunit4_test_baz.out1
+@@ -1 +1,3 @@
+-Bad result file
++Before Test
++Tested method
++After Test
+
 DocUnits:
 No doc units found
-Entities: 10; Documented ones: 0; With nitunits: 0; Failures: 0
+Entities: 12; Documented ones: 0; With nitunits: 0; Failures: 0
 
 TestSuites:
-Class suites: 1; Test Cases: 1; Failures: 1
-<testsuites><testsuite package="test_nitunit4&gt;"></testsuite><testsuite package="test_nitunit4::nitunit4"></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_foo"><system-err></system-err><system-out>out</system-out><error message="Before Test
+Class suites: 1; Test Cases: 3; Failures: 2
+<testsuites><testsuite package="test_nitunit4&gt;"></testsuite><testsuite package="test_nitunit4::nitunit4"></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_foo"><error>Runtime Error</error><system-err>Before Test
 Tested method
 After Test
 Runtime error: Assert failed (test_nitunit4&#47;test_nitunit4_base.nit:31)
-"></error></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
+</system-err></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_bar"></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_baz"><error>Runtime Error</error><system-err>Diff
+--- expected:test_nitunit4&#47;test_nitunit4.sav&#47;test_baz.res
++++ got:.nitunit&#47;gen_test_nitunit4_test_baz.out1
+@@ -1 +1,3 @@
+-Bad result file
++Before Test
++Tested method
++After Test
+</system-err></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index f40c047..54cb7f8 100644 (file)
@@ -43,3 +43,6 @@ test.has_suffix("test") => true
 test.has_suffix("bt") => false
 test.has_suffix("bat") => false
 test.has_suffix("foot") => false
+........
+
+........
diff --git a/tests/sav/xymus_net.res b/tests/sav/xymus_net.res
deleted file mode 100644 (file)
index e69de29..0000000
index db734a6..cd13aca 100644 (file)
@@ -22,5 +22,14 @@ class TestTestSuite
        fun test_foo do
                print "Tested method"
                assert before
+               before = false
+       end
+
+       fun test_bar do
+               print "Tested method"
+       end
+
+       fun test_baz do
+               print "Tested method"
        end
 end
diff --git a/tests/test_nitunit4/test_nitunit4.sav/test_bar.res b/tests/test_nitunit4/test_nitunit4.sav/test_bar.res
new file mode 100644 (file)
index 0000000..f6d97cf
--- /dev/null
@@ -0,0 +1,3 @@
+Before Test
+Tested method
+After Test
diff --git a/tests/test_nitunit4/test_nitunit4.sav/test_baz.res b/tests/test_nitunit4/test_nitunit4.sav/test_baz.res
new file mode 100644 (file)
index 0000000..0e89bcd
--- /dev/null
@@ -0,0 +1 @@
+Bad result file
index 3e7a1e7..af21e73 100644 (file)
@@ -28,6 +28,6 @@ class SuperTestSuite
 
        redef fun after_test do
                print "After Test"
-               assert false
+               assert before
        end
 end
index c2393aa..ed7d73d 100644 (file)
@@ -75,3 +75,6 @@ print("test.has_suffix(\"bt\") => {test.has_suffix("bt")}")
 print("test.has_suffix(\"bat\") => {test.has_suffix("bat")}")
 print("test.has_suffix(\"foot\") => {test.has_suffix("foot")}")
 
+print "........"
+print "/".substring(7, 1)
+print "........"