This wrapper works around the `native_postgres.nit` class and implements the minimal amount of functionality of that class to start a connection, use the execution methods, and inspect the results. It's the next phase in the postgres package, I've also added tests.
@xymus
Pull-Request: #2104
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>
c_src/** -diff
tests/sav/**/*.res -whitespace
+lib/popcorn/tests/res/*.res -whitespace
*.patch -whitespace
*.bak
*.swp
+*.swo
*~
.project
EIFGENs
bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit linux) ${NITC} pre-build
${NITC} src/asteronits.nit -m linux -o $@
-bin/texture_atlas_parser: src/texture_atlas_parser.nit
- ${NITC} src/texture_atlas_parser.nit -o $@
+bin/texture_atlas_parser: ../../lib/gamnit/texture_atlas_parser.nit
+ ${NITC} ../../lib/gamnit/texture_atlas_parser.nit -o $@
src/controls.nit: art/controls.svg
make -C ../inkscape_tools/
../inkscape_tools/bin/svg_to_png_and_nit art/controls.svg -a assets/ -s src/ -x 2.0 -g
-src/spritesheet_city.nit: bin/texture_atlas_parser
+src/spritesheet.nit: bin/texture_atlas_parser
bin/texture_atlas_parser art/sheet.xml --dir src/ -n spritesheet
-pre-build: src/controls.nit src/spritesheet_city.nit
+pre-build: src/controls.nit src/spritesheet.nit
check: bin/asteronits
NIT_TESTING=true bin/asteronits
-src/benitlux_restful.nit
+src/server/benitlux_restful.nit
*.db
*.email
benitlux_corrections.txt
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
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/
-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/>
- [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
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="item_background">#000000</color>
+</resources>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+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.
+.
[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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
# limitations under the License.
# Web server for Benitlux
-module benitlux_web
+module server
import benitlux_model
import benitlux_view
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# VR mode for Android with Google Cardboard
+#
+# This version is not playable and very laggy as it is not modified
+# or optimized in any way for VR.
+# This module is made available as a minimal example of a VR game.
+module tinks_vr
+
+import gamnit::vr
[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
redef fun on_create
do
# Create the main window
- window = new TnitterWindow
+ push_window new TnitterWindow
super
end
end
--- /dev/null
+all: xymus.net
+
+xymus.net: ../benitlux/src/server/benitlux_restful.nit $(shell ../../bin/nitls -M xymus_net.nit)
+ ../../bin/nitc -o $@ xymus_net.nit
+
+../benitlux/src/server/benitlux_restful.nit:
+ make -C ../benitlux src/server/benitlux_restful.nit
+
+pre-build: ../benitlux/src/server/benitlux_restful.nit
--- /dev/null
+Web server source and config of xymus.net
+
+This module acts also as an example to merge multiple `nitcorn` projects into one server.
+
+See the server online at http://xymus.net/.
--- /dev/null
+[package]
+name=xymus_net
+tags=web,example
+maintainer=Alexis Laferrière <alexis.laf@xymus.net>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/contrib/xymus.net/
+git=https://github.com/nitlang/nit.git
+git.directory=contrib/xymus.net/
+homepage=http://xymus.net/
+issues=https://github.com/nitlang/nit/issues
# This file is part of NIT ( http://www.nitlanguage.org ).
#
-# Copyright 2014-2015 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2014-2016 Alexis Laferrière <alexis.laf@xymus.net>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# Use actions defined by contribs
import tnitter
import benitlux::benitlux_controller
+import benitlux::benitlux_restful
import opportunity::opportunity_controller
import nitiwiki::wiki_edit
var tnitter_vh = new VirtualHost("tnitter.xymus.net:80")
var pep8_vh = new VirtualHost("pep8.xymus.net:80")
var benitlux_vh = new VirtualHost("benitlux.xymus.net:80")
+var benitlux_admin_vh = new VirtualHost("localhost:8081")
var factory = new HttpFactory.and_libevent
factory.config.virtual_hosts.add default_vh
factory.config.virtual_hosts.add tnitter_vh
factory.config.virtual_hosts.add pep8_vh
factory.config.virtual_hosts.add benitlux_vh
+factory.config.virtual_hosts.add benitlux_admin_vh
# Ports are open, drop to a low-privileged user if we are root
var user_group = new UserGroup("nitcorn", "nitcorn")
benitlux_vh.routes.add new Route("/static/", shared_file_server)
benitlux_vh.routes.add new Route(null, benitlux_sub)
+benitlux_admin_vh.routes.add new Route(null, new BenitluxAdminAction(benitlux_db))
+
# Opportunity service
var opportunity = new OpportunityWelcome
var opportunity_rest = new OpportunityRESTAction
[package]
name=calculator
-tags=example
+tags=example,mobile
maintainer=Alexis Laferrière <alexis.laf@xymus.net>
license=Apache-2.0
[upstream]
if debug then print "App::on_create"
# Create the main window
- window = new CalculatorWindow
+ push_window new CalculatorWindow
super
end
end
# See: <http://rosettacode.org/wiki/Perlin_noise>
module perlin_noise
-redef universal Float
- # Smoothened `self`
- fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0)
-end
-
-# Improved noise
-class ImprovedNoise
- # Permutations
- var p: Array[Int] = [151,160,137,91,90,15,
- 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
- 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
- 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
- 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
- 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
- 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
- 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
- 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
- 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
- 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
- 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
- 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]
-
- # Noise value in [-1..1] at 3d coordinates `x, y, z`
- fun noise(x, y, z: Float): Float
- do
- var xx = x.to_i & 255
- var yy = y.to_i & 255
- var zz = z.to_i & 255
-
- x -= x.floor
- y -= y.floor
- z -= z.floor
-
- var u = x.fade
- var v = y.fade
- var w = z.fade
-
- var a = p[xx ] + yy
- var aa = p[a ] + zz
- var ab = p[a+1 ] + zz
- var b = p[xx+1] + yy
- var ba = p[b ] + zz
- var bb = p[b+1 ] + zz
-
- return w.lerp(v.lerp(u.lerp(grad(p[aa ], x, y, z ),
- grad(p[ba ], x-1.0, y, z )),
- u.lerp(grad(p[ab ], x, y-1.0, z ),
- grad(p[bb ], x-1.0, y-1.0, z ))),
- v.lerp(u.lerp(grad(p[aa+1], x, y, z-1.0),
- grad(p[ba+1], x-1.0, y, z-1.0)),
- u.lerp(grad(p[ab+1], x, y-1.0, z-1.0),
- grad(p[bb+1], x-1.0, y-1.0, z-1.0))))
- end
-
- # Value at a corner of the grid
- fun grad(hash: Int, x, y, z: Float): Float
- do
- var h = hash & 15
- var u = if h < 8 then x else y
- var v = if h < 4 then y else if h == 12 or h == 14 then x else z
- return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v)
- end
-end
+import noise
var map = new ImprovedNoise
print map.noise(3.14, 42.0, 7.0).to_precision(17)
end
end
+ # Find the closest node accepted by `cond` under `max_cost`
+ fun find_closest(max_cost: Int, context: PathContext, cond: nullable TargetCondition[N]): nullable N
+ do
+ var path = path_to_alts(null, max_cost, context, cond)
+ if path == null then return null
+ return path.nodes.last
+ end
+
# We customize the serialization process to avoid problems with recursive
# serialization engines. These engines, such as `JsonSerializer`,
# are at danger to serialize the graph as a very deep tree.
import android.app.Activity;
import android.os.Bundle;
+import android.view.KeyEvent;
/*
* Entry point to Nit applications on Android, redirect most calls to Nit
protected native void nitOnDestroy(int activity);
protected native void nitOnSaveInstanceState(int activity, Bundle savedInstanceState);
protected native void nitOnRestoreInstanceState(int activity, Bundle savedInstanceState);
+ protected native boolean nitOnBackPressed(int activity);
+ protected native boolean nitOnKeyDown(int activity, int keyCode, KeyEvent event);
+ protected native boolean nitOnKeyLongPress(int activity, int keyCode, KeyEvent event);
+ protected native boolean nitOnKeyMultiple(int activity, int keyCode, int count, KeyEvent event);
+ protected native boolean nitOnKeyUp(int activity, int keyCode, KeyEvent event);
/*
* Implementation of OS callbacks
super.onRestoreInstanceState(savedInstanceState);
nitOnRestoreInstanceState(nitActivity, savedInstanceState);
}
+
+ @Override
+ public void onBackPressed() {
+ if (!nitOnBackPressed(nitActivity))
+ super.onBackPressed();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return nitOnKeyDown(nitActivity, keyCode, event)
+ || super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return nitOnKeyLongPress(nitActivity, keyCode, event)
+ || super.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+ return nitOnKeyMultiple(nitActivity, keyCode, count, event)
+ || super.onKeyMultiple(keyCode, count, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return nitOnKeyUp(nitActivity, keyCode, event)
+ || super.onKeyUp(keyCode, event);
+ }
}
`}
fun action: AMotionEventAction `{ return AMotionEvent_getAction(self); `}
+
+ fun native_down_time: Int `{ return AMotionEvent_getDownTime(self); `}
end
private extern class AMotionEventAction `{ int32_t `}
return null
end
end
+
+ # Time when the user originally pressed down to start a stream of position events
+ #
+ # The return value is in the `java.lang.System.nanoTime()` time base.
+ fun down_time: Int do return native.native_down_time
end
# A pointer event
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module key_event
+
+import platform
+
+# Java class: android.view.KeyEvent
+extern class NativeKeyEvent in "Java" `{ android.view.KeyEvent `}
+ super JavaObject
+
+ # Java implementation: boolean android.view.KeyEvent.isSystem()
+ fun is_system: Bool in "Java" `{
+ return self.isSystem();
+ `}
+
+ # Java implementation: android.view.KeyEvent.setSource(int)
+ fun set_source(arg0: Int) in "Java" `{
+ self.setSource((int)arg0);
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getMetaState()
+ fun meta_state: Int in "Java" `{
+ return self.getMetaState();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getModifiers()
+ fun modifiers: Int in "Java" `{
+ return self.getModifiers();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getFlags()
+ fun flags: Int in "Java" `{
+ return self.getFlags();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.hasNoModifiers()
+ fun has_no_modifiers: Bool in "Java" `{
+ return self.hasNoModifiers();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.hasModifiers(int)
+ fun has_modifiers(arg0: Int): Bool in "Java" `{
+ return self.hasModifiers((int)arg0);
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isAltPressed()
+ fun is_alt_pressed: Bool in "Java" `{
+ return self.isAltPressed();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isShiftPressed()
+ fun is_shift_pressed: Bool in "Java" `{
+ return self.isShiftPressed();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isSymPressed()
+ fun is_sym_pressed: Bool in "Java" `{
+ return self.isSymPressed();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isCtrlPressed()
+ fun is_ctrl_pressed: Bool in "Java" `{
+ return self.isCtrlPressed();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isMetaPressed()
+ fun is_meta_pressed: Bool in "Java" `{
+ return self.isMetaPressed();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isFunctionPressed()
+ fun is_function_pressed: Bool in "Java" `{
+ return self.isFunctionPressed();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isCapsLockOn()
+ fun is_caps_lock_on: Bool in "Java" `{
+ return self.isCapsLockOn();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isNumLockOn()
+ fun is_num_lock_on: Bool in "Java" `{
+ return self.isNumLockOn();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isScrollLockOn()
+ fun is_scroll_lock_on: Bool in "Java" `{
+ return self.isScrollLockOn();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getAction()
+ fun action: Int in "Java" `{
+ return self.getAction();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isCanceled()
+ fun is_canceled: Bool in "Java" `{
+ return self.isCanceled();
+ `}
+
+ # Java implementation: android.view.KeyEvent.startTracking()
+ fun start_tracking in "Java" `{
+ self.startTracking();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isTracking()
+ fun is_tracking: Bool in "Java" `{
+ return self.isTracking();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isLongPress()
+ fun is_long_press: Bool in "Java" `{
+ return self.isLongPress();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getKeyCode()
+ fun key_code: Int in "Java" `{
+ return self.getKeyCode();
+ `}
+
+ # Java implementation: java.lang.String android.view.KeyEvent.getCharacters()
+ fun characters: JavaString in "Java" `{
+ return self.getCharacters();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getScanCode()
+ fun scan_code: Int in "Java" `{
+ return self.getScanCode();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getRepeatCount()
+ fun repeat_count: Int in "Java" `{
+ return self.getRepeatCount();
+ `}
+
+ # Java implementation: long android.view.KeyEvent.getDownTime()
+ fun down_time: Int in "Java" `{
+ return self.getDownTime();
+ `}
+
+ # Java implementation: long android.view.KeyEvent.getEventTime()
+ fun event_time: Int in "Java" `{
+ return self.getEventTime();
+ `}
+
+ # Java implementation: char android.view.KeyEvent.getDisplayLabel()
+ fun display_label: Char in "Java" `{
+ return self.getDisplayLabel();
+ `}
+
+ # Java implementation: int android.view.KeyEvent.getUnicodeChar()
+ fun unicode_char: Int in "Java" `{
+ return self.getUnicodeChar();
+ `}
+
+ # Java implementation: char android.view.KeyEvent.getNumber()
+ fun number: Char in "Java" `{
+ return self.getNumber();
+ `}
+
+ # Java implementation: boolean android.view.KeyEvent.isPrintingKey()
+ fun is_printing_key: Bool in "Java" `{
+ return self.isPrintingKey();
+ `}
+
+ redef fun new_global_ref import sys, Sys.jni_env `{
+ Sys sys = NativeKeyEvent_sys(self);
+ JNIEnv *env = Sys_jni_env(sys);
+ return (*env)->NewGlobalRef(env, self);
+ `}
+
+ redef fun pop_from_local_frame_with_env(jni_env) `{
+ return (*jni_env)->PopLocalFrame(jni_env, self);
+ `}
+end
+
+# Java getter: android.view.KeyEvent.KEYCODE_BACK
+fun android_view_key_event_keycode_back: Int in "Java" `{
+ return android.view.KeyEvent.KEYCODE_BACK;
+`}
import platform
import log
import activities
+import key_event
import bundle
import dalvik
{
Activity_on_restore_instance_state((Activity)nit_activity, saved_state);
}
+
+ JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnBackPressed
+ (JNIEnv *env, jobject java_activity, jint nit_activity)
+ {
+ return (jboolean)Activity_on_back_pressed((Activity)nit_activity);
+ }
+
+ JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyDown
+ (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+ {
+ return (jboolean)Activity_on_key_down((Activity)nit_activity, keyCode, event);
+ }
+
+ JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyLongPress
+ (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+ {
+ return (jboolean)Activity_on_key_long_press((Activity)nit_activity, keyCode, event);
+ }
+
+ JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyMultiple
+ (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jint count, jobject event)
+ {
+ return (jboolean)Activity_on_key_multiple((Activity)nit_activity, keyCode, count, event);
+ }
+
+ JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyUp
+ (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+ {
+ return (jboolean)Activity_on_key_up((Activity)nit_activity, keyCode, event);
+ }
`}
# Wrapper to our Java `NitActivity`
Activity.on_create, Activity.on_destroy,
Activity.on_start, Activity.on_restart, Activity.on_stop,
Activity.on_pause, Activity.on_resume,
- Activity.on_save_instance_state, Activity.on_restore_instance_state `{
+ Activity.on_save_instance_state, Activity.on_restore_instance_state,
+ Activity.on_back_pressed,
+ Activity.on_key_down, Activity.on_key_long_press,
+ Activity.on_key_multiple, Activity.on_key_up `{
App_incr_ref(self);
global_app = self;
`}
# Notification from Android, the current device configuration has changed
fun on_configuration_changed do end
+
+ # The back key has been pressed
+ #
+ # Return `true` if the event has been handled.
+ fun on_back_pressed: Bool do return false
+
+ # A key has been pressed
+ #
+ # Return `true` if the event has been handled.
+ fun on_key_down(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+ # A key has been long pressed
+ #
+ # Return `true` if the event has been handled.
+ fun on_key_long_press(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+ # Multiple down/up pairs of the same key have occurred in a row
+ #
+ # Return `true` if the event has been handled.
+ fun on_key_multiple(key_code, count: Int, event: NativeKeyEvent): Bool do return false
+
+ # A key has been released
+ #
+ # Return `true` if the event has been handled.
+ fun on_key_up(key_code: Int, event: NativeKeyEvent): Bool do return false
end
# Set up global data in C and leave it to Android to callback Java, which we relay to Nit
# limitations under the License.
# Views and services to use the Android native user interface
-module ui
+module ui is
+ # `adjustPan` allows to use EditText in a ListLayout
+ android_manifest_activity """android:windowSoftInputMode="adjustPan""""
+end
# Implementation note:
#
end
end
+redef class Activity
+ redef fun on_back_pressed
+ do
+ var window = app.window
+ if window.enable_back_button then
+ window.on_back_button
+ return true
+ end
+
+ return false
+ end
+end
+
# On Android, a window is implemented with the fragment `native`
redef class Window
redef var native = (new Android_app_Fragment(self)).new_global_ref
else // if (align > 0.5d)
g = android.view.Gravity.RIGHT;
- view.setGravity(g);
+ view.setGravity(g | android.view.Gravity.CENTER_VERTICAL);
`}
end
redef class CheckBox
redef type NATIVE: Android_widget_CompoundButton
redef var native do return (new Android_widget_CheckBox(app.native_activity)).new_global_ref
+ init do set_callback_on_toggle(native)
redef fun is_checked do return native.is_checked
redef fun is_checked=(value) do native.set_checked(value)
+
+ private fun on_toggle do notify_observers new ToggleEvent(self)
+
+ private fun set_callback_on_toggle(view: NATIVE)
+ import on_toggle in "Java" `{
+ final int final_sender_object = self;
+ CheckBox_incr_ref(final_sender_object);
+
+ view.setOnCheckedChangeListener(
+ new android.widget.CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
+ CheckBox_on_toggle(final_sender_object);
+ }
+ });
+ `}
end
redef class TextInput
# The current `Window` of this activity
#
- # This attribute must be set by refinements of `App`.
- var window: Window is writable
+ # This attribute is set by `push_window`.
+ var window: Window is noinit
+
+ # Make visible and push `window` on the top of `pop_window`
+ #
+ # This method must be called at least once within `App::on_create`.
+ # It can be called at any times while the app is active.
+ fun push_window(window: Window)
+ do
+ window_stack.add window
+ self.window = window
+ end
+
+ # Pop the current `window` from the stack and show the previous one
+ #
+ # Require: `window_stack.not_empty`
+ fun pop_window
+ do
+ assert window_stack.not_empty
+ window_stack.pop
+ window = window_stack.last
+ window.on_resume
+ end
+
+ # Stack of active windows
+ var window_stack = new Array[Window]
redef fun on_create do window.on_create
# A window, root of the `Control` tree
class Window
super CompositeControl
+
+ # Should the back button be shown and used to go back to a previous window?
+ fun enable_back_button: Bool do return app.window_stack.length > 1
+
+ # The back button has been pressed, usually to open the previous window
+ fun on_back_button do app.pop_window
end
# A viewable `Control`
var is_checked = false is writable
end
+# Event sent from a `VIEW`
+class ViewEvent
+ super AppEvent
+
+ # The `VIEW` that raised this event
+ var sender: VIEW
+
+ # Type of the `sender`
+ type VIEW: View
+end
+
# A `Button` press event
class ButtonPressEvent
- super AppEvent
+ super ViewEvent
+
+ redef type VIEW: Button
+end
+
+# The `CheckBox` `sender` has been toggled
+class ToggleEvent
+ super ViewEvent
- # The `Button` that raised this event
- var sender: Button
+ redef type VIEW: CheckBox
end
# A layout to visually organize `Control`s
module bucketed_game is serialize
import serialization
+import counter
# Something acting on the game
abstract class Turnable[G: Game]
private var buckets: Array[BUCKET] =
[for b in n_buckets.times do new HashSet[Bucketable[G]]] is lazy
+ # Stats on delays asked when adding an event with `act_in` and `act_next`
+ private var delays = new Counter[Int]
+
# Add the Bucketable event `e` at `at_tick`.
fun add_at(e: Bucketable[G], at_tick: Int)
do
end
end
end
+
+ # Get some statistics on both the current held events and historic expired events
+ fun stats: String
+ do
+ var entries = 0
+ var instances = new HashSet[Bucketable[G]]
+ var max = 0
+ var min = 100000
+ for bucket in buckets do
+ var len = bucket.length
+ entries += len
+ instances.add_all bucket
+ min = min.min(len)
+ max = max.max(len)
+ end
+ var avg = entries.to_f / buckets.length.to_f
+
+ return "{buckets.length} buckets; uniq/tot:{instances.length}/{entries}, avg:{avg.to_precision(1)}, min:{min}, max:{max}\n" +
+ "history:{delays.sum}, avg:{delays.avg}, min:{delays[delays.min.as(not null)]}, max:{delays[delays.max.as(not null)]}"
+ end
end
# Game related event
# Game logic on the client
class ThinGame
- # Game tick when `self` should act.
+ # Current game tick
#
# Default is 0.
- var tick: Int = 0 is protected writable
+ var tick: Int = 0 is writable
end
# Game turn on the client
class ThinGameTurn[G: ThinGame]
- # Game tick when `self` should act.
+ # Game tick when happened this turn
var tick: Int is protected writable
# Game events occurred for `self`.
end
# Insert the Bucketable event `e` to be executed at next tick.
- fun act_next(e: Bucketable[G]) do game.buckets.add_at(e, tick + 1)
+ fun act_next(e: Bucketable[G])
+ do
+ game.buckets.add_at(e, tick + 1)
+ game.buckets.delays.inc(1)
+ end
# Insert the Bucketable event `e` to be executed at tick `t`.
- fun act_in(e: Bucketable[G], t: Int) do game.buckets.add_at(e, tick + t)
+ fun act_in(e: Bucketable[G], t: Int)
+ do
+ game.buckets.add_at(e, tick + t)
+ game.buckets.delays.inc(t)
+ end
# Add and `apply` a game `event`.
fun add_event( event : GameEvent )
end
redef fun substring(from, count) do
+ var ln = _length
+ if count <= 0 then return ""
+ if (count + from) > ln then count = ln - from
if count <= 0 then return ""
-
if from < 0 then
count += from
- if count < 0 then return ""
+ if count <= 0 then return ""
from = 0
end
- var ln = _length
- if (count + from) > ln then count = ln - from
return new ASCIIFlatString.full_data(_items, count, from + _first_byte, count)
end
yaw = 0.0
roll = 0.0
end
+
+ # Convert the position `x, y` on screen, to world coordinates on the plane at `target_z`
+ #
+ # `target_z` defaults to `0.0` and specifies the Z coordinates of the plane
+ # on which to project the screen position `x, y`.
+ #
+ # This method assumes that the camera is looking along the Z axis towards higher values.
+ # Using it in a different orientation can be useful, but won't result in valid
+ # world coordinates.
+ fun camera_to_world(x, y: Numeric, target_z: nullable Float): Point[Float]
+ do
+ # TODO, this method could be tweaked to support projecting the 2D point,
+ # on the near plane (x,y) onto a given distance no matter to orientation
+ # of the camera.
+
+ target_z = target_z or else 0.0
+
+ # Convert from pixel units / window resolution to
+ # units on the near clipping wall to
+ # units on the target wall at Z = 0
+ var near_height = (field_of_view_y/2.0).tan * near
+ var cross_screen_to_near = near_height / (display.height.to_f/2.0)
+ var cross_near_to_target = (position.z - target_z) / near
+ var mod = cross_screen_to_near * cross_near_to_target * 1.72 # FIXME drop the magic number
+
+ var wx = position.x + (x.to_f-display.width.to_f/2.0) * mod
+ var wy = position.y - (y.to_f-display.height.to_f/2.0) * mod
+ return new Point[Float](wx, wy)
+ end
end
# Orthogonal camera to draw UI objects with services to work with screens of different sizes
# Coordinates on the texture per vertex
var texture_coords = new Array[Float] is lazy, writable
+ # `GLDrawMode` used to display this mesh, defaults to `gl_TRIANGLES`
+ fun draw_mode: GLDrawMode do return gl_TRIANGLES
+
# Create an UV sphere of `radius` with `n_meridians` and `n_parallels`
init uv_sphere(radius: Float, n_meridians, n_parallels: Int)
do
# Execute draw
if mesh.indices.is_empty then
- glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+ glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
else
- glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+ glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
end
end
end
var xd = sample_used_texture.offset_right - xa
var ya = sample_used_texture.offset_top
var yd = sample_used_texture.offset_bottom - ya
+ xd *= 0.999
+ yd *= 0.999
var tex_coords = new Array[Float].with_capacity(mesh.texture_coords.length)
for i in [0..mesh.texture_coords.length/2[ do
tex_coords[i*2] = xa + xd * mesh.texture_coords[i*2]
- tex_coords[i*2+1] = ya + yd * mesh.texture_coords[i*2+1]
+ tex_coords[i*2+1] = 1.0 - (ya + yd * mesh.texture_coords[i*2+1])
end
program.tex_coord.array(tex_coords, 2)
program.camera.uniform(app.world_camera.position.x, app.world_camera.position.y, app.world_camera.position.z)
if mesh.indices.is_empty then
- glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+ glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
else
- glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+ glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
end
end
end
program.normal.array(mesh.normals, 3)
if mesh.indices.is_empty then
- glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+ glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
else
- glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+ glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
end
end
end
var d = [1.0, 0.0]
var texture_coords = new Array[Float]
- for v in [c, d, a, a, d, b] do for i in 6.times do texture_coords.add_all v
+ var face = [a, c, d, a, d, b]
+ for i in 6.times do for v in face do texture_coords.add_all v
return texture_coords
end
if leaves.is_empty then
# Nothing was loaded, use a cube with the default material
- var leaf = new LeafModel(new Cube, new SmoothMaterial.default)
+ var leaf = placeholder_model
leaves.add leaf
end
end
# Load textures need for these materials
for name in texture_names do
if not asset_textures_by_name.keys.has(name) then
- var tex = new GamnitAssetTexture(name)
+ var tex = new TextureAsset(name)
asset_textures_by_name[name] = tex
end
end
redef class Sys
# Textures loaded from .mtl files for models
- var asset_textures_by_name = new Map[String, GamnitAssetTexture]
+ var asset_textures_by_name = new Map[String, TextureAsset]
# All instantiated asset models
var models = new Set[ModelAsset]
+
+ # Blue cube of 1 unit on each side, acting as placeholder for models failing to load
+ #
+ # This model can be freely used by any `Actor` as placeholder or for debugging.
+ var placeholder_model = new LeafModel(new Cube, new SmoothMaterial.default) is lazy
end
# Only affects the desktop implementations.
var show_cursor: Bool = true is writable
+ # Number of bits used for the red value in the color buffer
+ fun red_bits: Int do return 8
+
+ # Number of bits used for the green value in the color buffer
+ fun green_bits: Int do return 8
+
+ # Number of bits used for the blue value in the color buffer
+ fun blue_bits: Int do return 8
+
# Prepare this display
#
# The implementation varies per platform.
setup_egl_display native_display
# We need 8 bits per color for selection by color
- select_egl_config(8, 8, 8, 0, 8, 0, 0)
+ select_egl_config(red_bits, green_bits, blue_bits, 0, 8, 0, 0)
var format = egl_config.attribs(egl_display).native_visual_id
native_window.set_buffers_geometry(0, 0, format)
redef fun close do close_egl
end
-redef class GamnitAssetTexture
+redef class TextureAsset
redef fun load_from_platform
do
setup_egl_display x11_display
if debug_gamnit then print "Setting up EGL context"
- select_egl_config(8, 8, 8, 8, 8, 0, 0)
+ select_egl_config(red_bits, green_bits, blue_bits, 8, 8, 0, 0)
setup_egl_context window_handle
end
end
end
-redef class GamnitAssetTexture
+redef class TextureAsset
redef fun load_from_platform
do
end
# Select an EGL config
- protected fun select_egl_config(blue, green, red, alpha, depth, stencil, sample: Int)
+ protected fun select_egl_config(red, green, blue, alpha, depth, stencil, sample: Int)
do
var config_chooser = new EGLConfigChooser
config_chooser.renderable_type_egl
config_chooser.surface_type_egl
- config_chooser.blue_size = blue
- config_chooser.green_size = green
config_chooser.red_size = red
+ config_chooser.green_size = green
+ config_chooser.blue_size = blue
if alpha > 0 then config_chooser.alpha_size = alpha
if depth > 0 then config_chooser.depth_size = depth
if stencil > 0 then config_chooser.stencil_size = stencil
# Current frame-rate
#
- # Updated each 5 seconds.
- var current_fps = 0.0
+ # Updated each 5 seconds, initialized at the value of `maximum_fps`.
+ var current_fps: Float = maximum_fps is lazy
redef fun frame_full
do
private var frame_count = 0
# Deadline used to compute `current_fps`
- private var frame_count_deadline = 0
+ private var frame_count_deadline = 5
# Check and sleep to maintain a frame-rate bellow `maximum_fps`
#
end
end
+ # Diagnose possible problems with the shaders of the program
+ #
+ # Lists to the console inactive uniforms and attributes.
+ # These may not be problematic but they can help to debug the program.
+ fun diagnose
+ do
+ if gl_program == null then compile_and_link
+
+ print "# Diagnose {class_name}"
+ for k,v in uniforms do
+ if not v.is_active then print "* Uniform {v.name} is inactive"
+ end
+ for k,v in attributes do
+ if not v.is_active then print "* Attribute {v.name} is inactive"
+ end
+ end
+
# Attributes of this program organized by name
#
# Active attributes are gathered at `compile_and_link`.
# Insert the first attribute, to load the root texture
var png_file = "images" / xml_file.basename("xml") + "png"
attributes.add """
- var root_texture = new Texture("{{{png_file}}}")"""
+ var root_texture = new TextureAsset("{{{png_file}}}")"""
# Read XML file
var content = xml_file.to_path.read_all
abstract class Texture
# Prepare a texture located at `path` within the `assets` folder
- new (path: Text) do return new GamnitAssetTexture(path.to_s)
+ new (path: Text) do return new TextureAsset(path.to_s)
# Root texture of which `self` is derived
fun root: GamnitRootTexture is abstract
# OpenGL handle to this texture
fun gl_texture: Int do return root.gl_texture
- # Prepare a subtexture from this texture
+ # Prepare a subtexture from this texture, from the given pixel offsets
fun subtexture(left, top, width, height: Numeric): GamnitSubtexture
do
# Setup the subtexture
end
# Texture loaded from the assets folder
-class GamnitAssetTexture
+class TextureAsset
super GamnitRootTexture
# Path to this texture within the `assets` folder
end
# Split a polygon into triangles
-# Useful for converting a concave polygon into multiple convex ones
-fun triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do
- var poly = new Polygon(pts)
- pts = poly.points
- recursive_triangulate(pts, results)
+#
+# Useful for converting a concave polygon into multiple convex ones.
+#
+# See: the alternative `triangulate_recursive` uses arrays in-place.
+fun triangulate(points: Array[Point[Float]]): Array[ConvexPolygon]
+do
+ var results = new Array[ConvexPolygon]
+ triangulate_recursive(points.clone, results)
+ return results
end
-private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do
- if pts.length == 3 then
- results.add(new ConvexPolygon(pts))
+# Split a polygon into triangles using arrays in-place
+#
+# Consumes the content of `points` and add the triangles to `results`.
+#
+# See: the alternative `triangulate` which does not modify `points` and returns a new array.
+fun triangulate_recursive(points: Array[Point[Float]], results: Array[ConvexPolygon]) do
+ if points.length == 3 then
+ results.add(new ConvexPolygon(points))
return
end
- var prev = pts[pts.length - 1]
- var curr = pts[0]
- var next = pts[1]
- for i in [1..pts.length[ do
+ var prev = points[points.length - 1]
+ var curr = points[0]
+ var next = points[1]
+ for i in [1..points.length[ do
if turn_left(prev, curr, next) then
- prev = pts[i-1]
+ prev = points[i-1]
curr = next
- if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1]
+ if i+1 == points.length then next = points[points.length - 1] else next = points[i+1]
continue
end
var contains = false
var triangle = new ConvexPolygon(new Array[Point[Float]].with_items(prev, curr, next))
- for p in pts do
+ for p in points do
if p != prev and p != curr and p != next then
if triangle.contain(p) then
contains = true
end
if not contains then
results.add(triangle)
- pts.remove(curr)
- recursive_triangulate(pts, results)
+ points.remove(curr)
+ triangulate_recursive(points, results)
break
end
- prev = pts[i-1]
+ prev = points[i-1]
curr = next
- if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1]
+ if i+1 == points.length then next = points[points.length - 1] else next = points[i+1]
end
end
@interface NitCallbackReference: NSObject
// Nit object target of the callbacks from UI events
- @property (nonatomic) Button nit_button;
+ @property (nonatomic) View nit_view;
// Actual callback method
- -(void) nitOnEvent: (UIButton*) sender;
+ -(void) nitOnEvent: (UIView*) sender;
@end
@implementation NitCallbackReference
- -(void) nitOnEvent: (UIButton*) sender {
- Button_on_click(self.nit_button);
+ -(void) nitOnEvent: (UIView*) sender {
+ View_on_ios_event(self.nit_view);
}
@end
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
redef type NATIVE: UIView
redef var enabled = null is lazy
+
+ private fun on_ios_event do end
end
redef class CompositeControl
init
do
native.alignment = new UIStackViewAlignment.fill
- native.distribution = new UIStackViewDistribution.fill_equally
# TODO make customizable
native.spacing = 4.0
end
redef class HorizontalLayout
- redef init do native.axis = new UILayoutConstraintAxis.horizontal
+ redef init
+ do
+ native.axis = new UILayoutConstraintAxis.horizontal
+ native.distribution = new UIStackViewDistribution.fill_equally
+ end
end
redef class VerticalLayout
- redef init do native.axis = new UILayoutConstraintAxis.vertical
+ redef init
+ do
+ native.axis = new UILayoutConstraintAxis.vertical
+ native.distribution = new UIStackViewDistribution.equal_spacing
+ end
end
redef class Label
# `UISwitch` acting as the real check box
var ui_switch: UISwitch is noautoinit
- init do
+ redef fun on_ios_event do notify_observers new ToggleEvent(self)
+
+ init
+ do
# Tweak the layout so it is centered
- layout.native.distribution = new UIStackViewDistribution.fill_proportionally
- layout.native.alignment = new UIStackViewAlignment.center
+ layout.native.distribution = new UIStackViewDistribution.equal_spacing
+ layout.native.alignment = new UIStackViewAlignment.fill
layout.native.layout_margins_relative_arrangement = true
var s = new UISwitch
native.add_arranged_subview s
ui_switch = s
+
+ ui_switch.set_callback self
end
redef fun text=(text) do lbl.text = text
redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
end
+redef class UISwitch
+ # Register callbacks on this switch to be relayed to `sender`
+ private fun set_callback(sender: View)
+ import View.on_ios_event in "ObjC" `{
+
+ NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
+ ncr.nit_view = sender;
+
+ // Pin the objects in both Objective-C and Nit GC
+ View_incr_ref(sender);
+ ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
+
+ [self addTarget:ncr action:@selector(nitOnEvent:)
+ forControlEvents:UIControlEventValueChanged];
+ `}
+end
+
redef class TextInput
redef type NATIVE: UITextField
init do native.set_callback self
+ redef fun on_ios_event do notify_observers new ButtonPressEvent(self)
+
redef fun text=(text) do if text != null then native.title = text.to_nsstring
redef fun text do return native.current_title.to_s
- private fun on_click do notify_observers new ButtonPressEvent(self)
-
redef fun enabled=(enabled) do native.enabled = enabled or else true
redef fun enabled do return native.enabled
end
redef class UIButton
# Register callbacks on this button to be relayed to `sender`
- private fun set_callback(sender: Button)
- import Button.on_click in "ObjC" `{
+ private fun set_callback(sender: View)
+ import View.on_ios_event in "ObjC" `{
NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
- ncr.nit_button = sender;
+ ncr.nit_view = sender;
// Pin the objects in both Objective-C and Nit GC
- Button_incr_ref(sender);
+ View_incr_ref(sender);
ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
[self addTarget:ncr action:@selector(nitOnEvent:)
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
bar.title = "app.nit" # TODO offer a portable API to name windows
bar.show_close_button = true
- # TODO add back button
+ bar.add back_button.native
return bar
end
return stack
end
+ # Button on the header bar to go back
+ var back_button = new BackButton is lazy
+
# On GNU/Linux, we go through all the callbacks once,
# there is no complex life-cycle.
redef fun run
app.on_start
app.on_resume
- native_window.show_all
gtk_main
app.on_pause
# improved with GTK 3.18 and interpolate_size.
native_window.resizable = false
+ native_window.show_all
+
super
+
+ if window.enable_back_button then
+ back_button.native.show
+ else back_button.native.hide
end
end
init do native.signal_connect("clicked", self, null)
end
+# Button to go back between windows
+class BackButton
+ super Button
+
+ # TODO i18n
+ redef fun text=(value) do super(value or else "Back")
+
+ redef fun signal(sender, data)
+ do
+ super
+
+ app.window.on_back_button
+ end
+end
+
redef class Label
redef type NATIVE: GtkLabel
redef var native = new GtkLabel("")
redef type NATIVE: GtkCheckButton
redef var native = new GtkCheckButton
+ redef fun signal(sender, data) do notify_observers new ToggleEvent(self)
+ init do native.signal_connect("toggled", self, null)
+
redef fun text do return native.text
redef fun text=(value) do native.text = (value or else "").to_s
A minimal example follows with a custom `Action` and using `FileServer`.
More general examples are available at `lib/nitcorn/examples/`.
-It includes the configuration of `http://xymus.net/` which merges many other _nitcorn_ applications.
+For an example of a larger project merging many _nitcorn_ applications into one server,
+take a look at the configuration of `http://xymus.net/` at `../contrib/xymus_net/xymus_net.nit`.
Larger projects using _nitcorn_ can be found in the `contrib/` folder:
* _opportunity_ is a meetup planner heavily based on _nitcorn_.
mkdir -p bin/
../../../bin/nitc --dir bin src/nitcorn_hello_world.nit src/simple_file_server.nit
-xymus.net:
- mkdir -p bin/
- ../../../bin/nitc --dir bin/ src/xymus_net.nit
-
pre-build: src/restful_annot_gen.nit
src/restful_annot_gen.nit:
../../../bin/nitrestful -o $@ src/restful_annot.nit
# 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
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
header_code = header.write_to_string
else header_code = ""
+ response = new HttpResponse(200)
response.body = """
<!DOCTYPE html>
<head>
</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
# Cache control
response.header["cache-control"] = cache_control
- end
+ else response = new HttpResponse(404)
else response = new HttpResponse(404)
else response = new HttpResponse(403)
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"
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
# Parameters match everything.
redef fun match(part) do return true
+
+ redef fun to_s do return name
end
# A static uri string like `users`.
# 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
return self & 0x3FFF_FFFF
end
end
+
+redef universal Float
+ # Smoothened `self`, used by `ImprovedNoise`
+ private fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0)
+end
+
+# Direct translation of Ken Perlin's improved noise Java implementation
+#
+# This implementation differs from `PerlinNoise` on two main points.
+# This noise is calculated for a 3D point, vs 2D in `PerlinNoise`.
+# `PerlinNoise` is based off a customizable seed, while this noise has a static data source.
+class ImprovedNoise
+
+ # Permutations
+ private var p: Array[Int] = [151,160,137,91,90,15,
+ 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
+ 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
+ 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
+ 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
+ 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
+ 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
+ 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
+ 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
+ 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
+ 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
+ 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
+ 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180] * 2
+
+ # Noise value in [-1..1] at 3D coordinates `x, y, z`
+ fun noise(x, y, z: Float): Float
+ do
+ var xx = x.floor.to_i & 255
+ var yy = y.floor.to_i & 255
+ var zz = z.floor.to_i & 255
+
+ x -= x.floor
+ y -= y.floor
+ z -= z.floor
+
+ var u = x.fade
+ var v = y.fade
+ var w = z.fade
+
+ var a = p[xx ] + yy
+ var aa = p[a ] + zz
+ var ab = p[a+1 ] + zz
+ var b = p[xx+1] + yy
+ var ba = p[b ] + zz
+ var bb = p[b+1 ] + zz
+
+ return w.lerp(v.lerp(u.lerp(grad(p[aa ], x, y, z ),
+ grad(p[ba ], x-1.0, y, z )),
+ u.lerp(grad(p[ab ], x, y-1.0, z ),
+ grad(p[bb ], x-1.0, y-1.0, z ))),
+ v.lerp(u.lerp(grad(p[aa+1], x, y, z-1.0),
+ grad(p[ba+1], x-1.0, y, z-1.0)),
+ u.lerp(grad(p[ab+1], x, y-1.0, z-1.0),
+ grad(p[bb+1], x-1.0, y-1.0, z-1.0))))
+ end
+
+ # Value at a corner of the grid
+ private fun grad(hash: Int, x, y, z: Float): Float
+ do
+ var h = hash & 15
+ var u = if h < 8 then x else y
+ var v = if h < 4 then y else if h == 12 or h == 14 then x else z
+ return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v)
+ end
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+NITUNIT=../../bin/nitunit
+
+check:
+ $(NITUNIT) README.md
+ $(NITUNIT) pop_routes.nit
+ $(NITUNIT) pop_handlers.nit
+ $(NITUNIT) popcorn.nit
+ cd tests; make check
--- /dev/null
+# 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.
--- /dev/null
+# 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)
--- /dev/null
+<!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>
--- /dev/null
+/*
+ * 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);
+ });
+})();
--- /dev/null
+<h1>Nit ♥ 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>
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# 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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/"))
+app.listen("localhost", 3000)
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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)
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <body>
+ <h1>Another Index</h1>
+ </body>
+</html>
--- /dev/null
+body {
+ color: blue;
+ padding: 20px;
+}
--- /dev/null
+<!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>
--- /dev/null
+alert("Hello World!");
--- /dev/null
+[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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# 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/
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import 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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[Client] curl -s localhost:*****/
+Hello World!
+[Client] curl -s localhost:*****/about
+An error occurred!
\ No newline at end of file
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+
+[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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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
--- /dev/null
+#!/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"
# then using a reference.
class SerializerCache
# Map of already serialized objects to the reference id
- private var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int]
+ protected var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int]
# Is `object` known?
fun has_object(object: Serializable): Bool do return sent.keys.has(object)
# Used by `Deserializer` to find already deserialized objects by their reference.
class DeserializerCache
# Map of references to already deserialized objects.
- private var received: Map[Int, Object] = new StrictHashMap[Int, Object]
+ protected var received: Map[Int, Object] = new StrictHashMap[Int, Object]
# Is there an object associated to `id`?
fun has_id(id: Int): Bool do return received.keys.has(id)
# 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.
#
-# 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>
&& ccache -C \
&& rm -rf .git
-# Dependencies for more libs and tests
-RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
- # Packages needed for lib/
- libcurl4-openssl-dev \
- libegl1-mesa-dev \
- libevent-dev \
- libgles1-mesa-dev \
- libgles2-mesa-dev \
- libgtk-3-dev \
- libncurses5-dev \
- libsdl-image1.2-dev \
- libsdl-ttf2.0-dev \
- libsdl1.2-dev \
- libsdl2-dev \
- libsqlite3-dev \
- libx11-dev \
- libxdg-basedir-dev \
- # Packages needed for platforms and FFI
- default-jdk \
- libopenmpi-dev \
- clang \
- # TODO neo4j android emscripten test_glsl_validation
- && rm -rf /var/lib/apt/lists/*
-
-# Run tests
-RUN cd /root/nit/tests \
- && ./testfull.sh || true \
- && rm -rf out/ alt/*.nit \
- && ccache -C
-# TODO: nitunits
-
-WORKDIR /root/nit
-ENTRYPOINT [ "bash" ]
+ENV NIT_DIR /root/nit
+ENV PATH $NIT_DIR/bin:$PATH
+WORKDIR $NIT_DIR
--- /dev/null
+# Supported tags and respective Dockerfile links
+
+* [latest](https://github.com/nitlang/nit/blob/master/misc/docker/Dockerfile)
+* [full](https://github.com/nitlang/nit/blob/master/misc/docker/full/Dockerfile)
+
+## What is Nit?
+
+Nit is an expressive language with a script-like syntax, a friendly type-system and aims at elegance, simplicity and intuitiveness.
+
+Nit has a simple straightforward style and can usually be picked up quickly, particularly by anyone who has programmed before.
+While object-oriented, it allows procedural styles.
+
+More information on
+
+* Website <http://www.nitlanguage.org>
+* Github <https://github.com/nitlang/nit>
+* Chatroom <https://gitter.im/nitlang/nit>
+
+## How to use this image
+
+You can use these images to build then run your programs.
+
+### Experimenting with Nit
+
+~~~
+host$ docker run -ti nitlang/nit
+root@ce9b671dd9fc:/root/nit# nitc examples/hello_world.nit
+root@ce9b671dd9fc:/root/nit# ./hello_world
+hello world
+~~~
+
+### Build and Run Programs
+
+In your Dockerfile, write something like:
+
+~~~Dockerfile
+FROM nitlang/nit
+
+# Create a workdir
+RUN mkdir -p /root/work
+WORKDIR /root/work
+
+# Copy the source code in /root/work/
+COPY . /root/work/
+
+# Compile
+RUN nitc src/hello.nit --dir . \
+ # Clear disk space
+ && ccache -C
+# You can also use a Makefile or any build system you want.
+
+# Run
+CMD ["./hello"]
+~~~
+
+Then, build and execute
+
+~~~
+host$ docker build -t nithello .
+host$ docker run --rm nithello
+hello!
+~~~
+
+See the full example at <https://github.com/nitlang/nit/blob/master/misc/docker/hello/Dockerfile>
--- /dev/null
+# 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" ]
--- /dev/null
+FROM nitlang/nit
+
+# Create a workdir
+RUN mkdir -p /root/work
+WORKDIR /root/work
+
+# Copy the source code in /root/work/
+COPY . /root/work/
+
+# Compile
+RUN nitc src/hello.nit --dir . \
+ # Clear disk space
+ && ccache -C
+# You can also use a Makefile or what you want
+
+# Say what to run
+CMD ["./hello"]
--- /dev/null
+print "hello"
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.
### `-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.
### `--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>
# 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]
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
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
import model_views
+redef class MEntity
+
+ # Collect modifier keywords like `redef`, `private` etc.
+ fun collect_modifiers: Array[String] do
+ return new Array[String]
+ end
+end
+
+redef class MPackage
+ redef fun collect_modifiers do
+ var res = super
+ res.add "package"
+ return res
+ end
+end
+
+redef class MGroup
+ redef fun collect_modifiers do
+ var res = super
+ res.add "group"
+ return res
+ end
+end
+
redef class MModule
+ redef fun collect_modifiers do
+ var res = super
+ res.add "module"
+ return res
+ end
+
# Collect all transitive imports.
fun collect_ancestors(view: ModelView): Set[MModule] do
var res = new HashSet[MModule]
redef class MClass
+ redef fun collect_modifiers do return intro.collect_modifiers
+
# Collect direct parents of `self` with `visibility >= to min_visibility`.
fun collect_parents(view: ModelView): Set[MClass] do
var res = new HashSet[MClass]
return res
end
- # Collect modifiers like redef, private etc.
- fun collect_modifiers: Array[String] do
- var res = new Array[String]
+ redef fun collect_modifiers do
+ var res = super
if not is_intro then
res.add "redef"
else
end
end
+redef class MProperty
+ redef fun collect_modifiers do return intro.collect_modifiers
+end
+
redef class MPropDef
- # Collect modifiers like redef, private, abstract, intern, fun etc.
- fun collect_modifiers: Array[String] do
- var res = new Array[String]
+ redef fun collect_modifiers do
+ var res = super
if not is_intro then
res.add "redef"
else
else
res.add "fun"
end
+ else if mprop isa MAttributeDef then
+ res.add "var"
end
return res
end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Make model entities Jsonable.
+#
+# To avoid cycles, every reference from a MEntity to another is replaced by a
+# MEntityRef.
+#
+# How subobjects are retrieved using the MEntityRef is the responsability of the
+# client. Json objects can be returned as this or inflated with concrete objet
+# rather than the refs.
+#
+# TODO consider serialization module?
+module model_json
+
+import model::model_collect
+import json
+import loader
+
+# A reference to another mentity.
+class MEntityRef
+ super MEntity
+
+ # MEntity to link to.
+ var mentity: MEntity
+
+ # Return `self` as a Json Object.
+ #
+ # By default, MEntity references contain only the `full_name` of the Mentity.
+ # You should redefine this method in your client to implement a different behavior.
+ redef fun json do
+ var obj = new JsonObject
+ obj["full_name"] = mentity.full_name
+ return obj
+ end
+end
+
+redef class MEntity
+ super Jsonable
+
+ # Return `self` as a JsonObject.
+ #
+ # By default, every reference to another MEntity is replaced by a pointer
+ # to the MEntity::json_id.
+ fun json: JsonObject do
+ var obj = new JsonObject
+ obj["name"] = name
+ obj["class_name"] = class_name
+ obj["full_name"] = full_name
+ obj["mdoc"] = mdoc_or_fallback
+ var modifiers = new JsonArray
+ for modifier in collect_modifiers do
+ modifiers.add modifier
+ end
+ obj["modifiers"] = modifiers
+ return obj
+ end
+
+ redef fun to_json do return json.to_json
+end
+
+redef class MDoc
+ super Jsonable
+
+ # Return `self` as a JsonObject.
+ fun json: JsonObject do
+ var obj = new JsonObject
+ obj["content"] = content.join("\n")
+ obj["location"] = location
+ return obj
+ end
+
+ redef fun to_json do return json.to_json
+end
+
+redef class Location
+ super Jsonable
+
+ # Return `self` as a JsonObject.
+ fun json: JsonObject do
+ var obj = new JsonObject
+ obj["column_end"] = column_end
+ obj["column_start"] = column_start
+ obj["line_end"] = line_end
+ obj["line_start"] = line_start
+ var file = self.file
+ if file != null then
+ obj["file"] = file.filename
+ end
+ return obj
+ end
+
+ redef fun to_json do return json.to_json
+end
+
+redef class MVisibility
+ super Jsonable
+
+ redef fun to_json do return to_s.to_json
+end
+
+redef class MPackage
+
+ redef fun json do
+ var obj = super
+ obj["visibility"] = public_visibility
+ if ini != null then
+ obj["ini"] = new JsonObject.from(ini.as(not null).to_map)
+ end
+ obj["root"] = to_mentity_ref(root)
+ obj["mgroups"] = to_mentity_refs(mgroups)
+ return obj
+ end
+end
+
+redef class MGroup
+ redef fun json do
+ var obj = super
+ obj["visibility"] = public_visibility
+ obj["is_root"] = is_root
+ obj["mpackage"] = to_mentity_ref(mpackage)
+ obj["default_mmodule"] = to_mentity_ref(default_mmodule)
+ obj["parent"] = to_mentity_ref(parent)
+ obj["mmodules"] = to_mentity_refs(mmodules)
+ obj["mgroups"] = to_mentity_refs(in_nesting.direct_smallers)
+ return obj
+ end
+end
+
+redef class MModule
+ redef fun json do
+ var obj = super
+ obj["location"] = location
+ obj["visibility"] = public_visibility
+ obj["mpackage"] = to_mentity_ref(mpackage)
+ obj["mgroup"] = to_mentity_ref(mgroup)
+ obj["intro_mclasses"] = to_mentity_refs(intro_mclasses)
+ obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+ return obj
+ end
+end
+
+redef class MClass
+ redef fun json do
+ var obj = super
+ obj["visibility"] = visibility
+ var arr = new JsonArray
+ for mparameter in mparameters do arr.add mparameter
+ obj["mparameters"] = arr
+ obj["intro"] = to_mentity_ref(intro)
+ obj["intro_mmodule"] = to_mentity_ref(intro_mmodule)
+ obj["mpackage"] = to_mentity_ref(intro_mmodule.mpackage)
+ obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+ return obj
+ end
+end
+
+redef class MClassDef
+ redef fun json do
+ var obj = super
+ obj["visibility"] = mclass.visibility
+ obj["location"] = location
+ obj["is_intro"] = is_intro
+ var arr = new JsonArray
+ for mparameter in mclass.mparameters do arr.add mparameter
+ obj["mparameters"] = arr
+ obj["mmodule"] = to_mentity_ref(mmodule)
+ obj["mclass"] = to_mentity_ref(mclass)
+ obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+ obj["intro_mproperties"] = to_mentity_refs(intro_mproperties)
+ return obj
+ end
+end
+
+redef class MProperty
+ redef fun json do
+ var obj = super
+ obj["visibility"] = visibility
+ obj["intro"] = to_mentity_ref(intro)
+ obj["intro_mclassdef"] = to_mentity_ref(intro_mclassdef)
+ obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+ return obj
+ end
+end
+
+redef class MMethod
+ redef fun json do
+ var obj = super
+ obj["is_init"] = is_init
+ obj["msignature"] = intro.msignature
+ return obj
+ end
+end
+
+redef class MAttribute
+ redef fun json do
+ var obj = super
+ obj["static_mtype"] = to_mentity_ref(intro.static_mtype)
+ return obj
+ end
+end
+
+redef class MVirtualTypeProp
+ redef fun json do
+ var obj = super
+ obj["mvirtualtype"] = to_mentity_ref(mvirtualtype)
+ obj["bound"] = to_mentity_ref(intro.bound)
+ return obj
+ end
+end
+
+redef class MPropDef
+ redef fun json do
+ var obj = super
+ obj["visibility"] = mproperty.visibility
+ obj["location"] = location
+ obj["is_intro"] = is_intro
+ obj["mclassdef"] = to_mentity_ref(mclassdef)
+ obj["mproperty"] = to_mentity_ref(mproperty)
+ return obj
+ end
+end
+
+redef class MMethodDef
+ redef fun json do
+ var obj = super
+ obj["msignature"] = msignature
+ return obj
+ end
+end
+
+redef class MAttributeDef
+ redef fun json do
+ var obj = super
+ obj["static_mtype"] = to_mentity_ref(static_mtype)
+ return obj
+ end
+end
+
+redef class MVirtualTypeDef
+ redef fun json do
+ var obj = super
+ obj["bound"] = to_mentity_ref(bound)
+ obj["is_fixed"] = is_fixed
+ return obj
+ end
+end
+
+redef class MSignature
+ redef fun json do
+ var obj = new JsonObject
+ obj["arity"] = arity
+ var arr = new JsonArray
+ for mparam in mparameters do arr.add mparam
+ obj["mparams"] = arr
+ obj["return_mtype"] = to_mentity_ref(return_mtype)
+ obj["vararg_rank"] = vararg_rank
+ return obj
+ end
+end
+
+redef class MParameterType
+ redef fun json do
+ var obj = new JsonObject
+ obj["name"] = name
+ obj["rank"] = rank
+ obj["mtype"] = to_mentity_ref(mclass.intro.bound_mtype.arguments[rank])
+ return obj
+ end
+end
+
+redef class MParameter
+ redef fun json do
+ var obj = new JsonObject
+ obj["is_vararg"] = is_vararg
+ obj["name"] = name
+ obj["mtype"] = to_mentity_ref(mtype)
+ return obj
+ end
+end
+
+# Create a ref to a `mentity`.
+fun to_mentity_ref(mentity: nullable MEntity): nullable MEntityRef do
+ if mentity == null then return null
+ return new MEntityRef(mentity)
+end
+
+# Return a collection of `mentities` as a JsonArray of MEntityRefs.
+fun to_mentity_refs(mentities: Collection[MEntity]): JsonArray do
+ var array = new JsonArray
+ for mentity in mentities do array.add to_mentity_ref(mentity)
+ return array
+end
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.
return
end
else
- if mprop.is_broken then
- return
- end
+ if mprop.is_broken then return
if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, not self isa AMainMethPropdef, mprop) then return
check_redef_property_visibility(modelbuilder, self.n_visibility, mprop)
end
if mreadprop == null then
var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility)
mreadprop = new MMethod(mclassdef, readname, self.location, mvisibility)
- if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then return
+ if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then
+ mreadprop.is_broken = true
+ return
+ end
else
+ if mreadprop.is_broken then return
if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, true, mreadprop) then return
check_redef_property_visibility(modelbuilder, self.n_visibility, mreadprop)
end
if mvisibility > protected_visibility then mvisibility = protected_visibility
end
mwriteprop = new MMethod(mclassdef, writename, self.location, mvisibility)
- if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then return
+ if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then
+ mwriteprop.is_broken = true
+ return
+ end
mwriteprop.deprecation = mreadprop.deprecation
else
+ if mwriteprop.is_broken then return
if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef or else n_kwredef, true, mwriteprop) then return
if atwritable != null then
check_redef_property_visibility(modelbuilder, atwritable.n_visibility, mwriteprop)
break
end
else
+ if mprop.is_broken then return
assert mprop isa MVirtualTypeProp
check_redef_property_visibility(modelbuilder, self.n_visibility, mprop)
end
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"
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
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
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)
"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
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
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*.
return ""
else
for arg in args do
- var format_error = "Syntax Eror: `{name}` expects its arguments to be of type Int or a call to `git_revision`."
-
var value
value = arg.as_int
if value != null then
# Get Git short revision
var proc = new ProcessReader("git", "rev-parse", "--short", "HEAD")
proc.wait
- assert proc.status == 0
+ if proc.status != 0 then
+ # Fallback if this is not a git repository or git bins are missing
+ version_fields.add "0"
+ modelbuilder.warning(self, "git_revision", "Warning: `git_revision` used outside of a git repository or git binaries not available")
+ continue
+ end
+
var lines = proc.read_all
var revision = lines.split("\n").first
continue
end
+ var format_error = "Syntax Error: `{name}` expects its arguments to be of type Int or a call to `git_revision`."
modelbuilder.error(self, format_error)
return ""
end
import modelize
private import parser_util
+import html
redef class ToolContext
# opt --full
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
if dir == null then return ".nitunit"
return dir
end
+
+ # Search the `nitc` compiler to use
+ #
+ # If not `nitc` is suitable, then prints an error and quit.
+ fun find_nitc: String
+ do
+ var nitc = opt_nitc.value
+ if nitc != null then
+ if not nitc.file_exists then
+ fatal_error(null, "error: cannot find `{nitc}` given by --nitc.")
+ abort
+ end
+ return nitc
+ end
+
+ nitc = "NITC".environ
+ if nitc != "" then
+ if not nitc.file_exists then
+ fatal_error(null, "error: cannot find `{nitc}` given by NITC.")
+ abort
+ end
+ return nitc
+ end
+
+ var nit_dir = nit_dir
+ nitc = nit_dir/"bin/nitc"
+ if not nitc.file_exists then
+ fatal_error(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.")
+ abort
+ end
+ return nitc
+ end
+
+ # Execute a system command in a more safe context than `Sys::system`.
+ fun safe_exec(command: String): Int
+ do
+ info(command, 2)
+ var real_command = """
+bash -c "
+ulimit -f {{{ulimit_file}}} 2> /dev/null
+ulimit -t {{{ulimit_usertime}}} 2> /dev/null
+{{{command}}}
+"
+"""
+ return system(real_command)
+ end
+
+ # The maximum size (in KB) of files written by a command executed trough `safe_exec`
+ #
+ # Default: 64MB
+ var ulimit_file = 65536 is writable
+
+ # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec`
+ #
+ # Default: 10 CPU minute
+ var ulimit_usertime = 600 is writable
+end
+
+# A unit test is an elementary test discovered, run and reported bu nitunit.
+#
+# This class factorizes `DocUnit` and `TestCase`.
+abstract class UnitTest
+
+ # Error occurred during test-case execution.
+ var error: nullable String = null is writable
+
+ # Was the test case executed at least once?
+ var was_exec = false is writable
+
+ # Return the `TestCase` in XML format compatible with Jenkins.
+ #
+ # See to_xml
+ fun to_xml: HTMLTag do
+ var tc = new HTMLTag("testcase")
+ tc.attr("classname", xml_classname)
+ tc.attr("name", xml_name)
+ var error = self.error
+ if error != null then
+ if was_exec then
+ tc.open("error").append("Runtime Error")
+ else
+ tc.open("failure").append("Compilation Error")
+ end
+ tc.open("system-err").append(error.trunc(8192).filter_nonprintable)
+ end
+ return tc
+ end
+
+ # The `classname` attribute of the XML format.
+ #
+ # NOTE: jenkins expects a '.' in the classname attr
+ fun xml_classname: String is abstract
+
+ # The `name` attribute of the XML format.
+ #
+ # See to_xml
+ fun xml_name: String is abstract
+end
+
+redef class String
+ # If needed, truncate `self` at `max_length` characters and append an informative `message`.
+ #
+ # ~~~
+ # assert "hello".trunc(10) == "hello"
+ # assert "hello".trunc(2) == "he[truncated. Full size is 5]"
+ # assert "hello".trunc(2, "...") == "he..."
+ # ~~~
+ fun trunc(max_length: Int, message: nullable String): String
+ do
+ if length <= max_length then return self
+ if message == null then message = "[truncated. Full size is {length}]"
+ return substring(0, max_length) + message
+ end
+
+ # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space).
+ #
+ # ~~~
+ # assert "hello".filter_nonprintable == "hello"
+ # assert "\r\n\t".filter_nonprintable == "^13\n^9"
+ # ~~~
+ fun filter_nonprintable: String
+ do
+ var buf = new Buffer
+ for c in self do
+ var cp = c.code_point
+ if cp < 32 and c != '\n' then
+ buf.append "^{cp}"
+ else
+ buf.add c
+ end
+ end
+ return buf.to_s
+ end
end
# The XML node associated to the module
var testsuite: HTMLTag
- # All blocks of code from a same `ADoc`
- var blocks = new Array[Buffer]
-
- # All failures from a same `ADoc`
- var failures = new Array[String]
-
# Markdown processor used to parse markdown comments and extract code.
var mdproc = new MarkdownProcessor
# used to generate distinct names
var cpt = 0
+ # The last docunit extracted from a mdoc.
+ #
+ # Is used because a new code-block might just be added to it.
+ var last_docunit: nullable DocUnit = null
+
+ var xml_classname: String is noautoinit
+
+ var xml_name: String is noautoinit
+
# The entry point for a new `ndoc` node
# Fill `docunits` with new discovered unit of tests.
- #
- # `tc` (testcase) is the pre-filled XML node
- fun extract(mdoc: MDoc, tc: HTMLTag)
+ fun extract(mdoc: MDoc, xml_classname, xml_name: String)
do
- blocks.clear
- failures.clear
+ last_docunit = null
+ self.xml_classname = xml_classname
+ self.xml_name = xml_name
self.mdoc = mdoc
mdproc.process(mdoc.content.join("\n"))
toolcontext.check_errors
-
- if not failures.is_empty then
- for msg in failures do
- var ne = new HTMLTag("failure")
- ne.attr("message", msg)
- tc.add ne
- toolcontext.modelbuilder.unit_entities += 1
- toolcontext.modelbuilder.failed_entities += 1
- end
- if blocks.is_empty then testsuite.add(tc)
- end
-
- if blocks.is_empty then return
- for block in blocks do
- docunits.add new DocUnit(mdoc, tc, block.write_to_string)
- end
end
# All extracted docunits
do
var simple_du = new Array[DocUnit]
for du in docunits do
+ # Skip existing errors
+ if du.error != null then continue
+
var ast = toolcontext.parse_something(du.block)
if ast isa AExpr then
simple_du.add du
end
test_simple_docunits(simple_du)
+
+ for du in docunits do
+ testsuite.add du.to_xml
+ end
end
# Executes multiples doc-units in a shared program.
i += 1
f.write("fun run_{i} do\n")
- f.write("# {du.testcase.attrs["name"]}\n")
+ f.write("# {du.full_name}\n")
f.write(du.block)
f.write("end\n")
end
i = 0
for du in dus do
- var tc = du.testcase
- toolcontext.modelbuilder.unit_entities += 1
i += 1
- toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1)
- var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 </dev/null")
-
- var msg
- f = new FileReader.open("{file}.out1")
- var n2
- n2 = new HTMLTag("system-err")
- tc.add n2
- msg = f.read_all
- f.close
+ toolcontext.info("Execute doc-unit {du.full_name} in {file} {i}", 1)
+ var res2 = toolcontext.safe_exec("{file.to_program_name}.bin {i} >'{file}.out1' 2>&1 </dev/null")
- n2 = new HTMLTag("system-out")
- tc.add n2
- n2.append(du.block)
+ var content = "{file}.out1".to_path.read_all
+ var msg = content.trunc(8192).filter_nonprintable
if res2 != 0 then
- var ne = new HTMLTag("error")
- ne.attr("message", msg)
- tc.add ne
- toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+ du.error = content
+ toolcontext.warning(du.location, "error", "ERROR: {du.full_name} (in {file}): Runtime error\n{msg}")
toolcontext.modelbuilder.failed_entities += 1
end
toolcontext.check_errors
-
- testsuite.add(tc)
end
end
# Used for docunits larger than a single block of code (with modules, classes, functions etc.)
fun test_single_docunit(du: DocUnit)
do
- var tc = du.testcase
- toolcontext.modelbuilder.unit_entities += 1
-
cpt += 1
var file = "{prefix}-{cpt}.nit"
- toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
+ toolcontext.info("Execute doc-unit {du.full_name} in {file}", 1)
var f
f = create_unitfile(file)
var res = compile_unitfile(file)
var res2 = 0
if res == 0 then
- res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
+ res2 = toolcontext.safe_exec("{file.to_program_name}.bin >'{file}.out1' 2>&1 </dev/null")
end
- var msg
- f = new FileReader.open("{file}.out1")
- var n2
- n2 = new HTMLTag("system-err")
- tc.add n2
- msg = f.read_all
- f.close
-
- n2 = new HTMLTag("system-out")
- tc.add n2
- n2.append(du.block)
-
+ var content = "{file}.out1".to_path.read_all
+ var msg = content.trunc(8192).filter_nonprintable
if res != 0 then
- var ne = new HTMLTag("failure")
- ne.attr("message", msg)
- tc.add ne
- toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+ du.error = content
+ toolcontext.warning(du.location, "failure", "FAILURE: {du.full_name} (in {file}):\n{msg}")
toolcontext.modelbuilder.failed_entities += 1
else if res2 != 0 then
- var ne = new HTMLTag("error")
- ne.attr("message", msg)
- tc.add ne
- toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+ toolcontext.warning(du.location, "error", "ERROR: {du.full_name} (in {file}):\n{msg}")
toolcontext.modelbuilder.failed_entities += 1
end
toolcontext.check_errors
-
- testsuite.add(tc)
end
# Create and fill the header of a unit file `file`.
# 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
end
executor.toolcontext.warning(location, "invalid-block", "{message} To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).")
- executor.failures.add("{location}: {message}")
+ executor.toolcontext.modelbuilder.failed_entities += 1
+
+ var du = new_docunit
+ du.block += code
+ du.error = "{location}: {message}"
return
end
# Create a first block
# Or create a new block for modules that are more than a main part
- if executor.blocks.is_empty or ast isa AModule then
- executor.blocks.add(new Buffer)
+ var last_docunit = executor.last_docunit
+ if last_docunit == null or ast isa AModule then
+ last_docunit = new_docunit
+ executor.last_docunit = last_docunit
end
# Add it to the file
- executor.blocks.last.append code
+ last_docunit.block += code
+
+ # In order to retrieve precise positions,
+ # the real position of each line of the raw_content is stored.
+ # See `DocUnit::real_location`
+ line_offset -= loc.line_start - 1
+ for i in [loc.line_start..loc.line_end] do
+ last_docunit.lines.add i + line_offset
+ last_docunit.columns.add column_offset
+ end
+ end
+
+ # Return and register a new empty docunit
+ fun new_docunit: DocUnit
+ do
+ var mdoc = executor.mdoc
+ assert mdoc != null
+
+ var next_number = 0
+ var name = executor.xml_name
+ if executor.docunits.not_empty and executor.docunits.last.mdoc == mdoc then
+ next_number = executor.docunits.last.number + 1
+ name += "+" + next_number.to_s
+ end
+
+ var res = new DocUnit(mdoc, next_number, "", executor.xml_classname, name)
+ executor.docunits.add res
+ executor.toolcontext.modelbuilder.unit_entities += 1
+ return res
end
end
-# A unit-test to run
+# A unit-test extracted from some documentation.
+#
+# A docunit is extracted from the code-blocks of mdocs.
+# Each mdoc can contains more than one docunit, and a single docunit can be made of more that a single code-block.
class DocUnit
+ super UnitTest
+
# The doc that contains self
var mdoc: MDoc
- # The XML node that contains the information about the execution
- var testcase: HTMLTag
+ # The numbering of self in mdoc (starting with 0)
+ var number: Int
+
+ # The name of the unit to show in messages
+ fun full_name: String do
+ var mentity = mdoc.original_mentity
+ if mentity != null then return mentity.full_name
+ return xml_classname + "." + xml_name
+ end
- # The text of the code to execute
+ # The text of the code to execute.
+ #
+ # This is the verbatim content on one, or more, code-blocks from `mdoc`
var block: String
+
+ # For each line in `block`, the associated line in the mdoc
+ #
+ # Is used to give precise locations
+ var lines = new Array[Int]
+
+ # For each line in `block`, the associated column in the mdoc
+ #
+ # Is used to give precise locations
+ var columns = new Array[Int]
+
+ # The location of the whole docunit.
+ #
+ # If `self` is made of multiple code-blocks, then the location
+ # starts at the first code-books and finish at the last one, thus includes anything between.
+ var location: Location is lazy do
+ return new Location(mdoc.location.file, lines.first, lines.last+1, columns.first+1, 0)
+ end
+
+ # Compute the real location of a node on the `ast` based on `mdoc.location`
+ #
+ # The result is basically: ast_location + markdown location of the piece + mdoc.location
+ #
+ # The fun is that a single docunit can be made of various pieces of code blocks.
+ fun real_location(ast_location: Location): Location
+ do
+ var mdoc = self.mdoc
+ var res = new Location(mdoc.location.file, lines[ast_location.line_start-1],
+ lines[ast_location.line_end-1],
+ columns[ast_location.line_start-1] + ast_location.column_start,
+ columns[ast_location.line_end-1] + ast_location.column_end)
+ return res
+ end
+
+ redef fun to_xml
+ do
+ var res = super
+ res.open("system-out").append(block)
+ return res
+ end
+
+ redef var xml_classname
+ redef var xml_name
end
redef class ModelBuilder
prefix = prefix.join_path(mmodule.to_s)
var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
- var tc
-
do
total_entities += 1
var nmoduledecl = nmodule.n_moduledecl
var ndoc = nmoduledecl.n_doc
if ndoc == null then break label x
doc_entities += 1
- tc = new HTMLTag("testcase")
# NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
- tc.attr("name", "<module>")
- d2m.extract(ndoc.to_mdoc, tc)
+ d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + ".<module>", "<module>")
end label x
for nclassdef in nmodule.n_classdefs do
var mclassdef = nclassdef.mclassdef
var ndoc = nclassdef.n_doc
if ndoc != null then
doc_entities += 1
- tc = new HTMLTag("testcase")
- tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
- tc.attr("name", "<class>")
- d2m.extract(ndoc.to_mdoc, tc)
+ d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, "<class>")
end
end
for npropdef in nclassdef.n_propdefs do
var ndoc = npropdef.n_doc
if ndoc != null then
doc_entities += 1
- tc = new HTMLTag("testcase")
- tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
- tc.attr("name", mpropdef.mproperty.full_name)
- d2m.extract(ndoc.to_mdoc, tc)
+ d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, mpropdef.mproperty.full_name)
end
end
end
prefix = prefix.join_path(mgroup.to_s)
var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
- var tc
-
total_entities += 1
var mdoc = mgroup.mdoc
if mdoc == null then return ts
doc_entities += 1
- tc = new HTMLTag("testcase")
# NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit." + mgroup.full_name)
- tc.attr("name", "<group>")
- d2m.extract(mdoc, tc)
+ d2m.extract(mdoc, "nitunit." + mgroup.full_name, "<group>")
d2m.run_tests
var prefix = toolcontext.test_dir / "file"
var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
- var tc
-
total_entities += 1
doc_entities += 1
- tc = new HTMLTag("testcase")
# NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit.<file>")
- tc.attr("name", file)
-
- d2m.extract(mdoc, tc)
+ d2m.extract(mdoc, "nitunit.<file>", file)
d2m.run_tests
return ts
# 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
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
# A test case is a unit test considering only a `MMethodDef`.
class TestCase
+ super UnitTest
# Test suite wich `self` belongs to.
var test_suite: TestSuite
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
toolcontext.warning(loc, "failure",
"ERROR: {method_name} (in file {test_file}.nit): {msg}")
toolcontext.modelbuilder.failed_tests += 1
+ else
+ var mmodule = test_method.mclassdef.mmodule
+ var file = mmodule.filepath
+ if file != null then
+ var sav = file.dirname / mmodule.name + ".sav" / test_method.name + ".res"
+ if sav.file_exists then
+ toolcontext.info("Diff output with {sav}", 1)
+ res = toolcontext.safe_exec("diff -u --label 'expected:{sav}' --label 'got:{res_name}.out1' '{sav}' '{res_name}.out1' > '{res_name}.diff' 2>&1 </dev/null")
+ if res != 0 then
+ msg = "Diff\n" + "{res_name}.diff".to_path.read_all
+ error = msg
+ toolcontext.warning(loc, "failure",
+ "ERROR: {method_name} (in file {test_file}.nit): {msg}")
+ toolcontext.modelbuilder.failed_tests += 1
+ end
+ else
+ toolcontext.info("No diff: {sav} not found", 2)
+ end
+ end
end
toolcontext.check_errors
end
- # Error occured during test-case execution.
- var error: nullable String = null
-
- # Was the test case executed at least one?
- var was_exec = false
-
- # Return the `TestCase` in XML format compatible with Jenkins.
- fun to_xml: HTMLTag do
+ redef fun xml_classname do
var mclassdef = test_method.mclassdef
- var tc = new HTMLTag("testcase")
- # NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name)
- tc.attr("name", test_method.mproperty.full_name)
- if was_exec then
- tc.add new HTMLTag("system-err")
- var n = new HTMLTag("system-out")
- n.append "out"
- tc.add n
- var error = self.error
- if error != null then
- n = new HTMLTag("error")
- n.attr("message", error.to_s)
- tc.add n
- end
- end
- return tc
+ return "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name
+ end
+
+ redef fun xml_name do
+ return test_method.mproperty.full_name
end
end
# * 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
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.
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
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
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.
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
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.
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
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
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
# 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
# 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
# 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
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
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
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
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
# 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
# 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
# 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
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
# 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
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>"
# 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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import core::kernel
+
+class A
+ fun f do 1.output
+ fun f2: Int do return 2
+ fun f2=(i:Int) do i.output
+ fun f3=(i:Int) do i.output
+ var v = 4
+ type T: A
+ fun t(t: T): T do return t
+end
+
+class B
+ super A#alt1#
+ redef fun f do 10.output
+ redef var f2 = 20
+ var f3 = 30 is redef writable
+ redef var v = 40
+ redef type T: B
+end
+
+class C
+ super B#alt2#
+ redef fun f do 100.output
+ redef var f2 = 200
+ redef var f3 = 300
+ redef var v = 400
+ redef type T: C
+
+end
+
+var a = new A
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
+
+a = new B
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
+
+a = new C
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
#!/bin/sh
-printf "%s\n" "$@" \
+ls -1 -- "%s\n" "$@" \
../src/nit*.nit \
../src/test_*.nit \
../src/examples/*.nit \
../examples/*/src/*_android.nit \
../examples/*/src/*_linux.nit \
../examples/*/src/*_null.nit \
- ../examples/nitcorn/src/*.nit \
../lib/*/examples/*.nit \
../lib/*/examples/*/*.nit \
../contrib/friendz/src/solver_cmd.nit \
../contrib/neo_doxygen/src/tests/neo_doxygen_*.nit \
../contrib/pep8analysis/src/pep8analysis.nit \
../contrib/nitiwiki/src/nitiwiki.nit \
- *.nit
+ *.nit \
+ | grep -v ../lib/popcorn/examples/
--- /dev/null
+1
+-2
+2
+-3
+1
+10
+-2
+10
+100
+-2
+100
--- /dev/null
+alt/base_redef_alt1.nit:29,12: Error: no property `B::f` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:30,12--13: Error: no property `B::f2` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:31,6--7: Error: no property `B::f3=` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:32,12: Error: no property `B::v` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:33,2--16: Error: no property `B::T` is inherited. Remove the `redef` keyword to define a new property.
--- /dev/null
+alt/base_redef_alt2.nit:38,12: Error: no property `C::f` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:39,12--13: Error: no property `C::f2` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:40,12--13: Error: no property `C::f3` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:41,12: Error: no property `C::v` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:42,2--16: Error: no property `C::T` is inherited. Remove the `redef` keyword to define a new property.
</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>
-test_nitunit.nit:20,1--22,0: ERROR: nitunit.test_nitunit::test_nitunit.test_nitunit::X.<class> (in .nitunit/test_nitunit-2.nit): Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5)
+test_nitunit.nit:21,7--22,0: ERROR: test_nitunit$X (in .nitunit/test_nitunit-2.nit):
+Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5)
-test_nitunit.nit:23,2--25,0: FAILURE: nitunit.test_nitunit::test_nitunit.test_nitunit::X.test_nitunit::X::foo (in .nitunit/test_nitunit-3.nit): .nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
+test_nitunit.nit:24,8--25,0: FAILURE: test_nitunit$X$foo (in .nitunit/test_nitunit-3.nit):
+.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
test_test_nitunit.nit:36,2--40,4: ERROR: test_foo1 (in file .nitunit/gen_test_test_nitunit.nit): Runtime error: Assert failed (test_test_nitunit.nit:39)
TestSuites:
Class suites: 1; Test Cases: 3; Failures: 1
-<testsuites><testsuite package="test_nitunit::test_nitunit"><testcase classname="nitunit.test_nitunit::test_nitunit.<module>" name="<module>"><system-err></system-err><system-out>assert true
-</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="<class>"><system-err></system-err><system-out>assert false
-</system-out><error message="Runtime error: Assert failed (.nitunit/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/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
-"></failure></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo"><system-err></system-err><system-out>out</system-out></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><system-err></system-err><system-out>out</system-out><error message="Runtime error: Assert failed (test_test_nitunit.nit:39)
-"></error></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"><system-err></system-err><system-out>out</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit::test_nitunit"><testcase classname="nitunit.test_nitunit::test_nitunit.<module>" name="<module>"><system-out>assert true
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="<class>"><system-out>assert false
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo"><failure>Compilation Error</failure><system-err>.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
+</system-err><system-out>assert undefined_identifier
+</system-out></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo"></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><error>Runtime Error</error><system-err>Runtime error: Assert failed (test_test_nitunit.nit:39)
+</system-err></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"></testcase></testsuite></testsuites>
\ No newline at end of file
TestSuites:
No test cases found
Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit2::test_nitunit2"><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo1"><system-err></system-err><system-out>if true then
+<testsuites><testsuite package="test_nitunit2::test_nitunit2"><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo1"><system-out>if true then
assert true
end
-</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::bar2"><system-err></system-err><system-out>if true then
+</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::bar2"><system-out>if true then
assert true
end
-</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo3"><system-err></system-err><system-out>var a = 1
+</system-out></testcase><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo3"><system-out>var a = 1
assert a == 1
assert a == 1
</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
TestSuites:
No test cases found
Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_doc2::test_doc2"><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo1"><system-err></system-err><system-out>assert true # tested
-</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo2"><system-err></system-err><system-out>assert true # tested
-</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo3"><system-err></system-err><system-out>assert true # tested
+<testsuites><testsuite package="test_doc2::test_doc2"><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo1"><system-out>assert true # tested
+</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo2"><system-out>assert true # tested
+</system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo3"><system-out>assert true # tested
</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\]. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).
-test_nitunit3/README.md:1,0--13,0: ERROR: nitunit.test_nitunit3>.<group> (in .nitunit/test_nitunit3-0.nit): Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
+test_nitunit3/README.md:4,2--15,0: ERROR: test_nitunit3> (in .nitunit/test_nitunit3-0.nit): Runtime error
+Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
DocUnits:
Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2
TestSuites:
No test cases found
Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit3>"><testcase classname="nitunit.test_nitunit3>" name="<group>"><failure message="test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\]."></failure><system-err></system-err><system-out>assert false
+<testsuites><testsuite package="test_nitunit3>"><testcase classname="nitunit.test_nitunit3>" name="<group>"><failure>Compilation Error</failure><system-err>Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
+</system-err><system-out>assert false
assert true
-</system-out><error message="Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
-"></error></testcase></testsuite><testsuite package="test_nitunit3::test_nitunit3"><testcase classname="nitunit.test_nitunit3::test_nitunit3.<module>" name="<module>"><system-err></system-err><system-out>assert true
+</system-out></testcase><testcase classname="nitunit.test_nitunit3>" name="<group>+1"><failure>Compilation Error</failure><system-err>test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\].</system-err><system-out>;'\][]
+</system-out></testcase></testsuite><testsuite package="test_nitunit3::test_nitunit3"><testcase classname="nitunit.test_nitunit3::test_nitunit3.<module>" name="<module>"><system-out>assert true
</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
-test_nitunit_md.md:1,0--15,0: ERROR: nitunit.<file>.test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error: Assert failed (.nitunit/file-0.nit:8)
+test_nitunit_md.md:4,2--16,0: ERROR: nitunit.<file>.test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error
+Runtime error: Assert failed (.nitunit/file-0.nit:8)
DocUnits:
Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1
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.<file>" 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.<file>" name="test_nitunit_md.md:1,0--15,0"><failure>Compilation Error</failure><system-err>Runtime error: Assert failed (.nitunit/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/file-0.nit:8)
-"></error></testcase></testsuite></testsuites>
\ No newline at end of file
+</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
TestSuites:
No test cases found
Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure message="test_doc3.nit:17,9--15: Syntax Error: unexpected identifier 'garbage'."></failure></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure message="test_doc3.nit:23,4--10: Syntax Error: unexpected identifier 'garbage'."></failure></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure message="test_doc3.nit:30,4--10: Syntax Error: unexpected identifier 'garbage'."></failure></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure>Compilation Error</failure><system-err>test_doc3.nit:17,9--15: Syntax Error: unexpected identifier 'garbage'.</system-err><system-out> *garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure>Compilation Error</failure><system-err>test_doc3.nit:23,4--10: Syntax Error: unexpected identifier 'garbage'.</system-err><system-out>*garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure>Compilation Error</failure><system-err>test_doc3.nit:30,4--10: Syntax Error: unexpected identifier 'garbage'.</system-err><system-out>*garbage*
+</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
-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>"></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>"></testsuite><testsuite package="test_nitunit4::nitunit4"></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_foo"><error>Runtime Error</error><system-err>Before Test
Tested method
After Test
Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31)
-"></error></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
+</system-err></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_bar"></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_baz"><error>Runtime Error</error><system-err>Diff
+--- expected:test_nitunit4/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
+</system-err></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
test.has_suffix("bt") => false
test.has_suffix("bat") => false
test.has_suffix("foot") => false
+........
+
+........
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
--- /dev/null
+Before Test
+Tested method
+After Test
--- /dev/null
+Bad result file
redef fun after_test do
print "After Test"
- assert false
+ assert before
end
end
print("test.has_suffix(\"bat\") => {test.has_suffix("bat")}")
print("test.has_suffix(\"foot\") => {test.has_suffix("foot")}")
+print "........"
+print "/".substring(7, 1)
+print "........"