Merge: src/platforms: fallback to version field "0" on error when asking for a git_re...
authorJean Privat <jean@pryen.org>
Sat, 21 May 2016 05:40:13 +0000 (01:40 -0400)
committerJean Privat <jean@pryen.org>
Sat, 21 May 2016 05:40:13 +0000 (01:40 -0400)
The _app.nit_ annotation `version` sets the version of mobile app packages (.apk and .app). For example, `version(1, 5, git_revision)` may produce the version string "1.5.6b42a7c".

This PR fixes an error when asking for a `git_revision` but the call to `git rev-parse` fails.
Note that the normal git error is printed before the nitc warning message, so debugging should be easy.

Close #2111

Pull-Request: #2113
Reviewed-by: Jean Privat <jean@pryen.org>

133 files changed:
.gitattributes
.gitignore
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/tnitter/package.ini
examples/calculator/package.ini
lib/android/ui/ui.nit
lib/ios/ui/ui.nit
lib/nitcorn/file_server.nit
lib/nitcorn/media_types.nit
lib/nitcorn/sessions.nit
lib/nitcorn/vararg_routes.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/template/macro.nit
misc/docker/Dockerfile
misc/docker/full/Dockerfile [new file with mode: 0644]
share/man/nitunit.md
src/catalog.nit
src/model/model_views.nit
src/nitcatalog.nit
src/nitunit.nit
src/nitweb.nit
src/parser/README.md
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/listfull.sh
tests/sav/nitcatalog_args1.res
tests/sav/nitunit_args1.res
tests/sav/nitunit_args6.res
tests/sav/nitunit_args7.res
tests/sav/nitunit_args9.res
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

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 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
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 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 1c40827..cd9f4ea 100644 (file)
@@ -244,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
 
index acf3018..48c7e12 100644 (file)
@@ -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
@@ -267,8 +280,8 @@ redef class CheckBox
        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
@@ -364,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 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
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 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..d7a3f49 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>
@@ -30,37 +29,3 @@ RUN git clone https://github.com/nitlang/nit.git /root/nit \
        && strip c_src/nitc bin/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" ]
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" ]
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 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 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 62cfb08..414de9a 100644 (file)
@@ -27,6 +27,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 +36,96 @@ 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
+
+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..05c1f67 100644 (file)
@@ -157,14 +157,15 @@ class NitUnitExecutor
                        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 res2 = toolcontext.safe_exec("{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
+                       var content = f.read_all
+                       var msg = content.trunc(8192).filter_nonprintable
+                       n2.append(msg)
                        f.close
 
                        n2 = new HTMLTag("system-out")
@@ -173,9 +174,9 @@ class NitUnitExecutor
 
                        if res2 != 0 then
                                var ne = new HTMLTag("error")
-                               ne.attr("message", msg)
+                               ne.attr("message", "Runtime error")
                                tc.add ne
-                               toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                               toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): Runtime error\n{msg}")
                                toolcontext.modelbuilder.failed_entities += 1
                        end
                        toolcontext.check_errors
@@ -206,15 +207,16 @@ 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
+               var content = f.read_all
+               var msg = content.trunc(8192).filter_nonprintable
+               n2.append(msg)
                f.close
 
                n2 = new HTMLTag("system-out")
@@ -224,15 +226,15 @@ class NitUnitExecutor
 
                if res != 0 then
                        var ne = new HTMLTag("failure")
-                       ne.attr("message", msg)
+                       ne.attr("message", "Compilation Error")
                        tc.add ne
-                       toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}):\n{msg}")
                        toolcontext.modelbuilder.failed_entities += 1
                else if res2 != 0 then
                        var ne = new HTMLTag("error")
-                       ne.attr("message", msg)
+                       ne.attr("message", "Runtime Error")
                        tc.add ne
-                       toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}):\n{msg}")
                        toolcontext.modelbuilder.failed_entities += 1
                end
                toolcontext.check_errors
@@ -268,18 +270,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
index 0de6e0a..ae1f4a2 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
@@ -253,7 +248,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,6 +259,25 @@ 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
@@ -282,14 +296,14 @@ class TestCase
                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  new HTMLTag("system-out")
+                       var n = new HTMLTag("system-err")
                        tc.add n
                        var error = self.error
                        if error != null then
+                               n.append error.trunc(8192).filter_nonprintable
                                n = new HTMLTag("error")
-                               n.attr("message", error.to_s)
+                               n.attr("message", "Runtime Error")
                                tc.add n
                        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
index 30c792b..a28ab28 100755 (executable)
@@ -1,5 +1,5 @@
 #!/bin/sh
-printf "%s\n" "$@" \
+ls -1 -- "%s\n" "$@" \
        ../src/nit*.nit \
        ../src/test_*.nit \
        ../src/examples/*.nit \
@@ -16,4 +16,5 @@ printf "%s\n" "$@" \
        ../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/
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..15eee20 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: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: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: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_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)
 
@@ -10,9 +12,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
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="&lt;class&gt;"><system-err>Runtime error: Assert failed (.nitunit&#47;test_nitunit-2.nit:5)
+</system-err><system-out>assert false
+</system-out><error message="Runtime Error"></error></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo"><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><failure message="Compilation Error"></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-out></system-out><system-err></system-err></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><system-out></system-out><system-err>Runtime error: Assert failed (test_test_nitunit.nit:39)
+</system-err><error message="Runtime Error"></error></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"><system-out></system-out><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index 34d34af..2a1e91e 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:1,0--13,0: ERROR: nitunit.test_nitunit3>.<group> (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,8 @@ 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 message="test_nitunit3&#47;README.md:7,3--5: Syntax Error: unexpected malformed character &#39;\]."></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><error message="Runtime error"></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></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index fc20a76..436061a 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:1,0--15,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"><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><error message="Runtime error"></error></testcase></testsuite></testsuites>
\ No newline at end of file
index de8f951..9cbce91 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"><system-out></system-out><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><error message="Runtime Error"></error></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_bar"><system-out></system-out><system-err></system-err></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_baz"><system-out></system-out><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><error message="Runtime Error"></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
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