This is a tiny wrapper for the GNU readline library. For some reasons, pkg-config doesn't know about libreadline so I had to hardcode the cflags using the ldflags module annotation.
Signed-off-by: Frédéric Vachon <fredvac@gmail.com>
Pull-Request: #2083
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Etienne M. Gagnon <egagnon@j-meg.com>
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
c_src/** -diff
tests/sav/**/*.res -whitespace
+lib/popcorn/tests/res/*.res -whitespace
*.patch -whitespace
*.bak
*.swp
+*.swo
*~
.project
EIFGENs
project ( http://nitlanguage.org ).
Files: *
-Copyright: 2004-2015 Jean Privat <jean@pryen.org>
+Copyright: 2004-2016 Jean Privat <jean@pryen.org>
2006-2008 Floréal Morandat <morandat@lirmm.fr>
2009 Julien Chevalier <chevjulien@gmail.com>
2009-2011 Jean-Sebastien Gelinas <calestar@gmail.com>
- 2009-2015 Alexis Laferrière <alexis.laf@xymus.net>
+ 2009-2016 Alexis Laferrière <alexis.laf@xymus.net>
2011 Matthieu Auger <matthieu.auger@gmail.com>
- 2011-2015 Alexandre Terrasa <alexandre@moz-code.org>
+ 2011-2016 Alexandre Terrasa <alexandre@moz-code.org>
2012 Alexandre Pennetier <alexandre.pennetier@me.com>
- 2013-2015 Lucas Bajolet <r4pass@hotmail.com>
+ 2013-2016 Lucas Bajolet <r4pass@hotmail.com>
2013 Stefan Lage <lagestfan@gmail.com>
2013 Nathan Heu <heu.nathan@courrier.uqam.ca>
2013 Matthieu Lucas <lucasmatthieu@gmail.com>
- 2014-2015 Romain Chanoir <romain.chanoir@viacesi.fr>
- 2014 Frédéric Vachon <fredvac@gmail.com>
+ 2014-2016 Romain Chanoir <romain.chanoir@viacesi.fr>
+ 2014-2015 Frédéric Vachon <fredvac@gmail.com>
2014 Johan Kayser <johan.kayser@viacesi.fr>
2014-2015 Julien Pagès <julien.projet@gmail.com>
2014 Geoffrey Hecht <geoffrey.hecht@gmail.com>
- 2014 Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
+ 2014-2016 Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
2015 Arthur Delamare <arthur.delamare@viacesi.fr>
+ 2015-2016 Mehdi Ait Younes <overpex@gmail.com>
+ 2015 Renata Carvalho <renatawm@gmail.com>
+ 2015 Simon Zeni <simonzeni@gmail.com>
+ 2015 Anis Boubaker <anis@boubaker.ca>
+ 2015 Istvan SZALAI <szalai972@gmail.com>
+ 2015 Hervé Matysiak <herve.matysiak@viacesi.fr>
+ 2015 Jean-Philippe Caissy <jean-philippe.caissy@shopify.com>
+ 2015 Alexandre Blondin Massé <alexandre.blondin.masse@gmail.com>
+ 2015-2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
License: Apache 2.0 (see LICENSE)
Comment: The main license of the work, except for the following exceptions
Files: lib/*
clib/*
share/nitdoc/*
-Copyright: 2004-2015 Jean Privat <jean@pryen.org>
+Copyright: 2004-2016 Jean Privat <jean@pryen.org>
2006-2008 Floréal Morandat <morandat@lirmm.fr>
2009-2011 Jean-Sebastien Gelinas <calestar@gmail.com>
- 2009-2015 Alexis Laferrière <alexis.laf@xymus.net>
+ 2009-2016 Alexis Laferrière <alexis.laf@xymus.net>
2009 Julien Chevalier <chevjulien@gmail.com>
- 2011-2015 Alexandre Terrasa <alexandre@moz-concept.com>
+ 2011-2016 Alexandre Terrasa <alexandre@moz-concept.com>
2012 Alexandre Pennetier <alexandre.pennetier@me.com>
- 2013-2015 Lucas Bajolet <r4pass@hotmail.com>
+ 2013-2016 Lucas Bajolet <r4pass@hotmail.com>
2013 Nathan Heu <heu.nathan@courrier.uqam.ca>
2013 Matthieu Lucas <lucasmatthieu@gmail.com>
2013 Stefan Lage <lagestfan@gmail.com>
2014 Maxime Leroy <maxime.leroy76@gmail.com>
2014 Johann Dubois <johann.dubois@outlook.com>
2014-2015 Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
- 2014 Alexandre Blondin Massé <alexandre.blondin.masse@gmail.com>
+ 2014-2015 Alexandre Blondin Massé <alexandre.blondin.masse@gmail.com>
+ 2015 Mehdi Ait Younes <overpex@gmail.com>
+ 2015 Budi Kurniawan <budi2020@gmail.com>
+ 2015-2016 Philippe Pepos Petitclerc <ppeposp@gmail.com>
+ 2015-2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
Licence: BSD 2 (see LICENSE-BSD)
Comment: Use of libraries and resources is basically unrestricted. We hold the copyright
on the compiler and the tools but not on the programs made by the users.
--- /dev/null
+# CSV Bench
+
+This is a simple benchmark for CSV parsers between different languages.
+
+Are required for testing (all packages can be apt-get'd):
+
+* Python 2
+* Python 3
+* Java JDK 6+, Apache-commons-csv
+* Ruby
+* Go language compiler
--- /dev/null
+#!/bin/bash
+# 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.
+
+# Shell script to bench json parsers over different documents
+
+source ../bench_common.sh
+source ../bench_plot.sh
+
+## CONFIGURATION OPTIONS ##
+
+# Default number of times a command must be run with bench_command
+# Can be overrided with 'the option -n'
+count=5
+
+## HANDLE OPTIONS ##
+
+function init_repo()
+{
+ mkdir -p inputs
+ nitc --semi-global scripts/csv_gen.nit -o scripts/csv_gen
+ echo "Generating 1000 lines documents"
+ ./scripts/csv_gen 10 1000 inputs/1000_l.csv
+ ./scripts/csv_gen 10 1000 inputs/1000_uni_l.csv --unicode
+ echo "Generating 10000 lines documents"
+ ./scripts/csv_gen 10 10000 inputs/10000_l.csv
+ ./scripts/csv_gen 10 10000 inputs/10000_uni_l.csv --unicode
+ echo "Generating 100000 lines documents"
+ ./scripts/csv_gen 10 100000 inputs/100000_l.csv
+ ./scripts/csv_gen 10 100000 inputs/100000_uni_l.csv --unicode
+ echo "Generating 1000000 lines documents"
+ ./scripts/csv_gen 10 1000000 inputs/1000000_l.csv
+ ./scripts/csv_gen 10 1000000 inputs/1000000_uni_l.csv --unicode
+}
+
+function usage()
+{
+ echo "run_bench: ./csv_bench.sh [options]"
+ echo " -v: verbose mode"
+ echo " -n count: number of execution for each bar (default: $count)"
+ echo " -h: this help"
+}
+
+stop=false
+fast=false
+while [ "$stop" = false ]; do
+ case "$1" in
+ -v) verbose=true; shift;;
+ --fast) fast=true; shift;;
+ -h) usage; exit;;
+ -n) count="$2"; shift; shift;;
+ *) stop=true
+ esac
+done
+
+if [ -z "$fast" ]; then
+ init_repo
+fi
+
+mkdir -p out
+
+echo "Compiling engines"
+
+echo "Java Parser"
+
+javac -cp './scripts/commons-csv-1.3.jar' scripts/JavaCSV.java
+
+echo "Go parser"
+
+go build -o scripts/go_csv scripts/go_csv.go
+
+echo "Nit/Ad-Hoc Parser"
+
+nitc --semi-global scripts/nit_csv.nit -o scripts/nit_csv
+
+declare -a script_names=('Python 3 - Pandas' 'Python 2 - Pandas' 'Go' 'Nit' 'Python 3 - Standard' 'Python 2 - Standard' 'Java - Apache commons' 'Ruby')
+declare -a script_cmds=('python3 scripts/python_csv.py' 'python2 scripts/python_csv.py' './scripts/go_csv' './scripts/nit_csv' 'python3 scripts/python_stdcsv.py' 'python2 scripts/python_stdcsv.py' "java -cp /usr/share/java/commons-csv.jar:. scripts.JavaCSV" 'ruby scripts/ruby_csv.rb')
+
+for script in `seq 1 ${#script_cmds[@]}`; do
+ echo "Preparing res for ${script_names[$script - 1]}"
+ prepare_res "./out/${script_names[$script - 1]}.dat" "${script_names[$script - 1]}" "${script_names[$script - 1]}"
+ for file in inputs/*.csv; do
+ fname=`basename $file .csv`
+ bench_command $file "Benching file $file using ${script_cmds[$script - 1]} parser" ${script_cmds[$script - 1]} $file
+ done;
+done;
+
+rm scripts/nit_csv
+rm scripts/JavaCSV.class
+rm scripts/go_csv
+
+plot out/bench_csv.gnu
--- /dev/null
+package scripts;
+
+import java.io.File;
+import java.util.List;
+import java.nio.charset.Charset;
+import org.apache.commons.csv.*;
+
+class JavaCSV {
+ public static void main(String[] args) {
+ try {
+ File csvData = new File(args[0]);
+ CSVParser parser = CSVParser.parse(csvData, Charset.forName("UTF-8"), CSVFormat.RFC4180);
+ List<CSVRecord> r = parser.getRecords();
+ } catch(Exception e) {
+ System.err.println("Major fail");
+ }
+ }
+}
--- /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 csv
+
+if args.length < 3 then
+ print "Usage ./csv_gen record_length record_nb out_filepath [--unicode]"
+ exit 1
+end
+
+var record_length = args[0].to_i
+var record_nb = args[1].to_i
+var outpath = args[2]
+var unicode = false
+
+if args.length == 4 then
+ if not args[3] == "--unicode" then
+ print "Usage ./csv_gen record_length record_nb [--unicode]"
+ exit 1
+ end
+ unicode = true
+end
+
+var ocsv = new CsvDocument
+ocsv.eol = "\r\n"
+
+var sep = ocsv.separator.to_s
+var eol = ocsv.eol
+var del = ocsv.delimiter.to_s
+
+for i in [0 .. record_length[ do ocsv.header.add "Col{i}"
+
+var c = if unicode then "á" else "a"
+for i in [0 .. record_nb[ do
+ var line = new Array[String].with_capacity(record_length)
+ for j in [0 .. record_length[ do
+ var add_sep = 100.rand > 70
+ var add_del = 100.rand > 70
+ var add_eol = 100.rand > 70
+ var ln = 10.rand
+ var s = c * ln
+ if add_sep then s = sep + s
+ if add_del then s += del
+ if add_eol then s += eol
+ line.add s
+ end
+ ocsv.records.add line
+end
+
+ocsv.write_to_file(outpath)
--- /dev/null
+package main
+
+import "encoding/csv"
+import "os"
+import "fmt"
+
+func main() {
+ if len(os.Args) == 1 {
+ fmt.Println("Usage ./go_csv file")
+ os.Exit(-1)
+ }
+ file, err := os.Open(os.Args[1])
+ if err != nil { panic(err) }
+
+ var read = csv.NewReader(file)
+ _, r := read.ReadAll()
+ if r != nil { panic(err) }
+}
--- /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 csv
+
+if args.is_empty then
+ print "Usage: ./nit_csv in.csv"
+ exit 1
+end
+
+var csv = new CsvReader(new FileReader.open(args[0]))
+csv.eol = "\r\n"
+
+csv.read_all
--- /dev/null
+import sys
+from pandas import read_csv
+
+csv = read_csv(sys.argv[1])
--- /dev/null
+import sys
+import csv
+
+lst = list();
+with open(sys.argv[1], 'r') as f:
+ reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE)
+ for row in reader:
+ list.append(lst, row)
--- /dev/null
+require 'csv'
+
+CSV.read(ARGV.first)
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
# See the License for the specific language governing permissions and
# limitations under the License.
-import ::android::platform
+import ::android
import ::android::vibration
import asteronits
if dy > 0.0 then
# Bottom part of the joystick, turns left or right
if dx < 0.0 then
- ship.applied_rotation = -1.0
- else
ship.applied_rotation = 1.0
+ else
+ ship.applied_rotation = -1.0
end
else
# Upper part of the joystick, detect action using 45d angles
if dx < dy then
- ship.applied_rotation = -1.0
- else if dx > -dy then
ship.applied_rotation = 1.0
+ else if dx > -dy then
+ ship.applied_rotation = -1.0
else
ship.applied_thrust = 1.0
end
# Add the joystick to the UI
ui_sprites.add new Sprite(spritesheet_controls.forward,
- ui_camera.bottom_left.offset(joystick_x, -200.0, 0.0))
+ ui_camera.bottom_left.offset(joystick_x, 200.0, 0.0))
ui_sprites.add new Sprite(spritesheet_controls.left,
- ui_camera.bottom_left.offset(joystick_x-100.0, -joystick_y, 0.0))
+ ui_camera.bottom_left.offset(joystick_x-100.0, joystick_y, 0.0))
ui_sprites.add new Sprite(spritesheet_controls.right,
- ui_camera.bottom_left.offset(joystick_x+100.0, -joystick_y, 0.0))
+ ui_camera.bottom_left.offset(joystick_x+100.0, joystick_y, 0.0))
# Purely cosmetic joystick background
ui_sprites.add new Sprite(spritesheet_controls.joystick_back,
- ui_camera.bottom_left.offset(joystick_x, -joystick_y, -1.0)) # In the back
+ ui_camera.bottom_left.offset(joystick_x, joystick_y, -1.0)) # In the back
ui_sprites.add new Sprite(spritesheet_controls.joystick_down,
ui_camera.bottom_left.offset(joystick_x, 0.0, 1.0))
# Add the "open fire" button
ui_sprites.add new Sprite(spritesheet_controls.fire,
- ui_camera.bottom_right.offset(-150.0, -150.0, 0.0))
+ ui_camera.bottom_right.offset(-150.0, 150.0, 0.0))
end
end
-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
var users = new Array[User]
end
+# Daily menu notifications
+class DailyNotification
+ serialize
+
+ # All beers on the menu today
+ var beers: Array[BeerAndRatings]
+end
+
# Server or API usage error
class BenitluxError
super Error
--- /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
return new HttpResponse.ok(log)
end
+ # Is `token` valid?
+ #
+ # check_token?token=a -> true | BenitluxError
+ fun check_token(token: String): HttpResponse
+ is restful do
+ var user_id = db.token_to_id(token)
+ if user_id == null then return new HttpResponse.invalid_token
+ return new HttpResponse.ok(true)
+ end
+
# Search a user
#
# search?token=b&query=a&offset=0 -> Array[UserAndFollowing] | BenitluxError
end
# ---
+# Administration
+
+# Path to the secret used to authenticate admin requests
+fun secret_path: String do return "benitlux.secret"
+
+# Services reserved to administrators
+class BenitluxAdminAction
+ super BenitluxAction
+ super RestfulAction
+
+ private fun server_secret: String do return secret_path.to_path.read_all
+
+ # Trigger sending daily menu to connected clients
+ #
+ # This should usually be called by an external cron program.
+ # send_daily_updates?secret=shared_secret -> true | BenitluxError
+ fun send_daily_updates(secret: nullable String): HttpResponse
+ is restful do
+ # Check secrets
+ var server_secret = server_secret
+ if server_secret.is_empty then
+ print_error "The admin interface needs a secret at '{secret_path}'"
+ return new HttpResponse.server_error
+ end
+
+ if server_secret != secret then
+ return new HttpResponse.invalid_token
+ end
+
+ # Load beer menu
+ var list = db.list_beers_and_rating
+ if list == null then return new HttpResponse.server_error
+
+ var msg = new DailyNotification(list)
+
+ # Broadcast updates
+ for conn in push_connections.values.to_a do
+ if not conn.closed then
+ conn.respond new HttpResponse.ok(msg)
+ conn.close
+ end
+ end
+ push_connections.clear
+
+ return new HttpResponse.ok(true)
+ end
+
+ redef fun answer(request, turi) do return new HttpResponse.bad_request
+end
+
+# ---
# Misc services
redef class Text
init ok(data: Serializable)
do
init 200
- body = data.to_json_string
+ body = data.serialize_to_json
end
# Respond with a `BenitluxError` in JSON and a code 403
do
init 403
var error = new BenitluxTokenError("Forbidden", "Invalid or outdated token.")
- body = error.to_json_string
+ body = error.serialize_to_json
end
# Respond with a `BenitluxError` in JSON and a code 400
do
init 400
var error = new BenitluxError("Bad Request", "Application error, or it needs to be updated.")
- body = error.to_json_string
+ body = error.serialize_to_json
end
# Respond with a `BenitluxError` in JSON and a code 500
do
init 500
var error = new BenitluxError("Internal Server Error", "Server error, try again later.")
- body = error.to_json_string
+ body = error.serialize_to_json
end
end
last_weekday = "date('now', 'weekday 6', '-7 day')"
else last_weekday = "date('now', '-1 day')"
- return beer_events_since(last_weekday)
+ return beer_events_since_sql(last_weekday)
end
# Build and return a `BeerEvents` for today compared to `prev_day`
# Return `null` on error
fun beer_events_since(prev_day: String): nullable BeerEvents
do
+ prev_day = prev_day.to_sql_date_string
+ return beer_events_since_sql("date({prev_day})")
+ end
+
+ # `BeerEvents` since the SQLite formatted date command `sql_date`
+ #
+ # Return `null` on error
+ private fun beer_events_since_sql(sql_date: String): nullable BeerEvents
+ do
var events = new BeerEvents
# New
var stmt = select("ROWID, name, desc FROM beers WHERE " +
"ROWID IN (SELECT beer FROM daily WHERE day=(SELECT MAX(day) FROM daily)) AND " +
- "NOT ROWID IN (SELECT beer FROM daily WHERE date(day) = date({prev_day}))")
+ "NOT ROWID IN (SELECT beer FROM daily WHERE date(day) = {sql_date})")
if stmt == null then return null
for row in stmt do events.new_beers.add row.to_beer
# Gone
stmt = select("ROWID, name, desc FROM beers WHERE " +
"NOT ROWID IN (SELECT beer FROM daily WHERE day=(SELECT MAX(day) FROM daily)) AND " +
- "ROWID IN (SELECT beer FROM daily WHERE date(day) = date({prev_day}))")
+ "ROWID IN (SELECT beer FROM daily WHERE date(day) = {sql_date})")
if stmt == null then return null
for row in stmt do events.gone_beers.add row.to_beer
# Fix
stmt = select("ROWID, name, desc FROM beers WHERE " +
"ROWID IN (SELECT beer FROM daily WHERE day=(SELECT MAX(day) FROM daily)) AND " +
- "ROWID IN (SELECT beer FROM daily WHERE date(day) = date({prev_day}))")
+ "ROWID IN (SELECT beer FROM daily WHERE date(day) = {sql_date})")
if stmt == null then return null
for row in stmt do events.fix_beers.add row.to_beer
# limitations under the License.
# Web server for Benitlux
-module benitlux_web
+module server
import benitlux_model
import benitlux_view
# Listening interface
fun iface: String do return "localhost:8080"
+# Listening interface for admin commands
+fun iface_admin: String do return "localhost:8081"
+
# Sqlite3 database
var db_path = "benitlux_sherbrooke.db"
var db = new BenitluxDB.open(db_path)
vh.routes.add new Route("/push/", new BenitluxPushAction(db))
vh.routes.add new Route(null, new BenitluxSubscriptionAction(db))
+var vh_admin = new VirtualHost(iface_admin)
+vh_admin.routes.add new Route(null, new BenitluxAdminAction(db))
+
var factory = new HttpFactory.and_libevent
factory.config.virtual_hosts.add vh
+factory.config.virtual_hosts.add vh_admin
-print "Launching server on http://{iface}/"
+print "Launching server on http://{iface}/ and http://{iface_admin}/"
factory.run
db.close
game.save
end
- # Maximum wanted frame per second
- var max_fps = 30
-
- # clock used to track FPS
- private var clock = new Clock
-
redef fun frame_core(display)
do
game.step
game.draw(display)
- var dt = clock.lapse
- var target_dt = 1000000000 / max_fps
- if dt.sec == 0 and dt.nanosec < target_dt then
- var sleep_t = target_dt - dt.nanosec
- sys.nanosleep(0, sleep_t)
- end
end
redef fun input(input_event)
# Gen a test db with a random name (to avoid race conditions).
fun gen_test_db: MongoDb do
- var db_name = "test_nitrpg_{get_time}_{1000.rand}"
+ var testid = "NIT_TESTING_ID".environ.to_i
+ var db_name = "test_nitrpg_{testid}"
var db = load_db(db_name)
test_dbs.add db
return db
# Save the default config to pretty Json
var cc = new ClientConfig
- var json = cc.to_plain_json
- json = json.replace(",", ",\n")
+ var json = cc.serialize_to_json(plain=true, pretty=true)
json.write_to_file config_path
return cc
--- /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
var dt = clock.lapse
tick += 1
- var turn = new TTurn(self, tick, dt.to_f, dt.millisec)
+ var turn = new TTurn(self, tick, dt, ((dt-dt.floor)*1000.0).to_i)
return turn
end
[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
db.close
var response = new HttpResponse(200)
- response.body = posts.to_json_string
+ response.body = posts.serialize_to_json
return response
end
# Format not recognized
var error = new Error("Bad Request")
var response = new HttpResponse(400)
- response.body = error.to_json_string
+ response.body = error.serialize_to_json
return response
end
end
# Everyone gets the same response
var posts = list_posts(0, 16)
var response = new HttpResponse(400)
- response.body = posts.to_json_string
+ response.body = posts.serialize_to_json
for conn in push_connections do
# Complete the answer to `conn`
redef fun on_create
do
# Create the main window
- window = new TnitterWindow
+ push_window new TnitterWindow
super
end
end
import tnitter_app
-import android::ui
-import android::http_request
+import android
import android::portrait
redef class LabelAuthor
--- /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]
module android_calculator
import calculator
-import android::ui
+import android
redef class Button
init do set_android_style(native, (text or else "?").is_int)
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);
+ }
}
only be used by low-level implementations of Nit on Android.
Its usefulness will be extended in the future to customize user applications.
-## Project entry points
+## Android implementation
+
+There is two core implementation for Nit apps on Android.
+`android::nit_activity` is used by apps with standard windows and native UI controls.
+`android::game` is used by, well, games and the game frameworks `mnit` and `gamnit`.
+
+Clients don't have to select the core implementation, it is imported by other relevant modules.
+For example, a module importing `app::ui` and `android` will trigger the importation of `android::nit_activity`.
+
+## Lock app orientation
Importing `android::landscape` or `android::portrait` locks the generated
application in the specified orientation. This can be useful for games and
module android
import platform
-import native_app_glue
import dalvik
private import log
-private import assets
-
-redef class App
- redef fun init_window
- do
- super
- on_create
- on_restore_state
- on_start
- end
-
- redef fun term_window
- do
- super
- on_stop
- end
-
- # Is the application currently paused?
- var paused = true
-
- redef fun pause
- do
- paused = true
- on_pause
- super
- end
-
- redef fun resume
- do
- paused = false
- on_resume
- super
- end
-
- redef fun save_state do on_save_state
-
- redef fun lost_focus
- do
- paused = true
- super
- end
-
- redef fun gained_focus
- do
- paused = false
- super
- end
-
- redef fun destroy do on_destroy
-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 services and implementation of app.nit for gamnit and mnit
+module game
+
+import platform
+import native_app_glue
+import dalvik
+private import log
+private import assets
+
+redef class App
+ redef fun init_window
+ do
+ super
+ on_create
+ on_restore_state
+ on_start
+ end
+
+ redef fun term_window
+ do
+ super
+ on_stop
+ end
+
+ # Is the application currently paused?
+ var paused = true
+
+ redef fun pause
+ do
+ paused = true
+ on_pause
+ super
+ end
+
+ redef fun resume
+ do
+ paused = false
+ on_resume
+ super
+ end
+
+ redef fun save_state do on_save_state
+
+ redef fun lost_focus
+ do
+ paused = true
+ super
+ end
+
+ redef fun gained_focus
+ do
+ paused = false
+ super
+ end
+
+ redef fun destroy do on_destroy
+end
module input_events
import mnit::input
-import android
+import android::game
in "C header" `{
#include <android/log.h>
`}
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;
+`}
android_manifest_activity """android:screenOrientation="sensorLandscape" """
end
-import platform
+import android
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
android:screenOrientation="portrait"
"""
-import platform
+import android
# See the License for the specific language governing permissions and
# limitations under the License.
-# This module is used to manipulate android sensors
-# The sensor support is implemented in android_app module, so the user can enable the type of sensor he wants to use.
-# There is an example of how you can use the android sensors in nit/examples/mnit_ballz :
+# Access Android sensors
+#
+# Sensors are to be enabled when `App` is created.
+# The following example enables all sensors.
+# The events (`SensorEvent`, `ASensorAccelerometer`, `ASensorMagneticField`...)
+# are sent to the `input` callback of `App`
#
# ~~~~nitish
-# #FIXME rewrite the example
# redef class App
-# sensors_support_enabled = true
-# accelerometer.enabled = true
-# accelerometer.eventrate = 10000
-# magnetic_field.enabled = true
-# gyroscope.enabled = true
-# light.enabled = true
-# proximity.enabled = true
+# init
+# do
+# sensors_support_enabled = true
+# accelerometer.enabled = true
+# accelerometer.eventrate = 10000
+# magnetic_field.enabled = true
+# gyroscope.enabled = true
+# light.enabled = true
+# proximity.enabled = true
+# end
# end
# ~~~~
-#
-# In this example, we enable the sensor support, then enable all types of sensors supported by the API, directly with `App` attributes
-# As a result, you get all type of SensorEvent (ASensorAccelerometer, ASensorMagneticField ...) in the `input` callback of `App`
module sensors
-import android
+import game
import mnit
in "C header" `{
# 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
import core::error
# Platform variations
-# TODO: move on the platform once qualified names are understand in the condition
import linux::audio is conditional(linux)
import android::audio is conditional(android)
import serialization
# Platform variations
-# TODO: move on the platform once qualified names are understand in the condition
import linux::data_store is conditional(linux)
import android::data_store is conditional(android)
import ios::data_store is conditional(ios)
import app_base
# Platform variations
-# TODO: move on the platform once qualified names are understand in the condition
import linux::ui is conditional(linux)
-import android::ui is conditional(android) # FIXME it should be conditional to `android::platform`
+import android::ui is conditional(android)
import ios::ui is conditional(ios)
redef class App
# The current `Window` of this activity
#
- # This attribute must be set by refinements of `App`.
- var window: Window is writable
+ # This attribute is set by `push_window`.
+ var window: Window is noinit
+
+ # Make visible and push `window` on the top of `pop_window`
+ #
+ # This method must be called at least once within `App::on_create`.
+ # It can be called at any times while the app is active.
+ fun push_window(window: Window)
+ do
+ window_stack.add window
+ self.window = window
+ end
+
+ # Pop the current `window` from the stack and show the previous one
+ #
+ # Require: `window_stack.not_empty`
+ fun pop_window
+ do
+ assert window_stack.not_empty
+ window_stack.pop
+ window = window_stack.last
+ window.on_resume
+ end
+
+ # Stack of active windows
+ var window_stack = new Array[Window]
redef fun on_create do window.on_create
# 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 )
do
return command.to_cstring.system
end
+
+ # The pid of the program
+ fun pid: Int `{ return getpid(); `}
end
redef class NativeString
return res
end
+ # Is `self` the path to an existing directory ?
+ #
+ # ~~~nit
+ # assert ".".to_path.is_dir
+ # assert not "/etc/issue".to_path.is_dir
+ # assert not "/should/not/exist".to_path.is_dir
+ # ~~~
+ fun is_dir: Bool do
+ var st = stat
+ if st == null then return false
+ return st.is_dir
+ end
+
# Delete a directory and all of its content
#
# Does not go through symbolic links and may get stuck in a cycle if there
# SEE: `abstract_text::Text` for more info on the difference
# between `Text::bytelen` and `Text::length`.
fun to_s_full(bytelen, unilen: Int): String is abstract
+
+ # Copies the content of `src` to `self`
+ #
+ # NOTE: `self` must be large enough to withold `self.bytelen` bytes
+ fun fill_from(src: Text) do src.copy_to_native(self, src.bytelen, 0, 0)
end
redef class NativeArray[E]
end
return res
end
+
+ redef fun copy_to_native(dst, n, src_off, dst_off) do
+ _items.copy_to(dst, n, first_byte + src_off, dst_off)
+ end
end
# Immutable strings of characters.
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
end
redef fun copy_to_native(dest, n, src_offset, dest_offset) do
- var subs = new RopeSubstrings.from(self, src_offset)
- var st = src_offset - subs.pos
- var off = dest_offset
- while n > 0 do
- var it = subs.item
- if n > it.length then
- var cplen = it.length - st
- it._items.copy_to(dest, cplen, st, off)
- off += cplen
- n -= cplen
- else
- it._items.copy_to(dest, n, st, off)
- n = 0
- end
- subs.next
- st = 0
+ var l = _left
+ if src_offset < l.bytelen then
+ var lcopy = l.bytelen - src_offset
+ lcopy = if lcopy > n then n else lcopy
+ l.copy_to_native(dest, lcopy, src_offset, dest_offset)
+ dest_offset += lcopy
+ n -= lcopy
+ src_offset = 0
end
+ _right.copy_to_native(dest, n, src_offset, dest_offset)
end
# Returns a balanced version of `self`
module crapto
import english_utils
+import xor
--- /dev/null
+HUIfTQsPAh9PE048GmllH0kcDk4TAQsHThsBFkU2AB4BSWQgVB0dQzNTTmVS
+BgBHVBwNRU0HBAxTEjwMHghJGgkRTxRMIRpHKwAFHUdZEQQJAGQmB1MANxYG
+DBoXQR0BUlQwXwAgEwoFR08SSAhFTmU+Fgk4RQYFCBpGB08fWXh+amI2DB0P
+QQ1IBlUaGwAdQnQEHgFJGgkRAlJ6f0kASDoAGhNJGk9FSA8dDVMEOgFSGQEL
+QRMGAEwxX1NiFQYHCQdUCxdBFBZJeTM1CxsBBQ9GB08dTnhOSCdSBAcMRVhI
+CEEATyBUCHQLHRlJAgAOFlwAUjBpZR9JAgJUAAELB04CEFMBJhAVTQIHAh9P
+G054MGk2UgoBCVQGBwlTTgIQUwg7EAYFSQ8PEE87ADpfRyscSWQzT1QCEFMa
+TwUWEXQMBk0PAg4DQ1JMPU4ALwtJDQhOFw0VVB1PDhxFXigLTRkBEgcKVVN4
+Tk9iBgELR1MdDAAAFwoFHww6Ql5NLgFBIg4cSTRWQWI1Bk9HKn47CE8BGwFT
+QjcEBx4MThUcDgYHKxpUKhdJGQZZVCFFVwcDBVMHMUV4LAcKQR0JUlk3TwAm
+HQdJEwATARNFTg5JFwQ5C15NHQYEGk94dzBDADsdHE4UVBUaDE5JTwgHRTkA
+Umc6AUETCgYAN1xGYlUKDxJTEUgsAA0ABwcXOwlSGQELQQcbE0c9GioWGgwc
+AgcHSAtPTgsAABY9C1VNCAINGxgXRHgwaWUfSQcJABkRRU8ZAUkDDTUWF01j
+OgkRTxVJKlZJJwFJHQYADUgRSAsWSR8KIgBSAAxOABoLUlQwW1RiGxpOCEtU
+YiROCk8gUwY1C1IJCAACEU8QRSxORTBSHQYGTlQJC1lOBAAXRTpCUh0FDxhU
+ZXhzLFtHJ1JbTkoNVDEAQU4bARZFOwsXTRAPRlQYE042WwAuGxoaAk5UHAoA
+ZCYdVBZ0ChQLSQMYVAcXQTwaUy1SBQsTAAAAAAAMCggHRSQJExRJGgkGAAdH
+MBoqER1JJ0dDFQZFRhsBAlMMIEUHHUkPDxBPH0EzXwArBkkdCFUaDEVHAQAN
+U29lSEBAWk44G09fDXhxTi0RAk4ITlQbCk0LTx4cCjBFeCsGHEETAB1EeFZV
+IRlFTi4AGAEORU4CEFMXPBwfCBpOAAAdHUMxVVUxUmM9ElARGgZBAg4PAQQz
+DB4EGhoIFwoKUDFbTCsWBg0OTwEbRSonSARTBDpFFwsPCwIATxNOPBpUKhMd
+Th5PAUgGQQBPCxYRdG87TQoPD1QbE0s9GkFiFAUXR0cdGgkADwENUwg1DhdN
+AQsTVBgXVHYaKkg7TgNHTB0DAAA9DgQACjpFX0BJPQAZHB1OeE5PYjYMAg5M
+FQBFKjoHDAEAcxZSAwZOBREBC0k2HQxiKwYbR0MVBkVUHBZJBwp0DRMDDk5r
+NhoGACFVVWUeBU4MRREYRVQcFgAdQnQRHU0OCxVUAgsAK05ZLhdJZChWERpF
+QQALSRwTMRdeTRkcABcbG0M9Gk0jGQwdR1ARGgNFDRtJeSchEVIDBhpBHQlS
+WTdPBzAXSQ9HTBsJA0UcQUl5bw0KB0oFAkETCgYANlVXKhcbC0sAGgdFUAIO
+ChZJdAsdTR0HDBFDUk43GkcrAAUdRyonBwpOTkJEUyo8RR8USSkOEENSSDdX
+RSAdDRdLAA0HEAAeHQYRBDYJC00MDxVUZSFQOV1IJwYdB0dXHRwNAA9PGgMK
+OwtTTSoBDBFPHU54W04mUhoPHgAdHEQAZGU/OjV6RSQMBwcNGA5SaTtfADsX
+GUJHWREYSQAnSARTBjsIGwNOTgkVHRYANFNLJ1IIThVIHQYKAGQmBwcKLAwR
+DB0HDxNPAU94Q083UhoaBkcTDRcAAgYCFkU1RQUEBwFBfjwdAChPTikBSR0T
+TwRIEVIXBgcURTULFk0OBxMYTwFUN0oAIQAQBwkHVGIzQQAGBR8EdCwRCEkH
+ElQcF0w0U05lUggAAwANBxAAHgoGAwkxRRMfDE4DARYbTn8aKmUxCBsURVQf
+DVlOGwEWRTIXFwwCHUEVHRcAMlVDKRsHSUdMHQMAAC0dCAkcdCIeGAxOazkA
+BEk2HQAjHA1OAFIbBxNJAEhJBxctDBwKSRoOVBwbTj8aQS4dBwlHKjUECQAa
+BxscEDMNUhkBC0ETBxdULFUAJQAGARFJGk9FVAYGGlMNMRcXTRoBDxNPeG43
+TQA7HRxJFUVUCQhBFAoNUwctRQYFDE43PT9SUDdJUydcSWRtcwANFVAHAU5T
+FjtFGgwbCkEYBhlFeFsABRcbAwZOVCYEWgdPYyARNRcGAQwKQRYWUlQwXwAg
+ExoLFAAcARFUBwFOUwImCgcDDU5rIAcXUj0dU2IcBk4TUh0YFUkASEkcC3QI
+GwMMQkE9SB8AMk9TNlIOCxNUHQZCAAoAHh1FXjYCDBsFABkOBkk7FgALVQRO
+D0EaDwxOSU8dGgI8EVIBAAUEVA5SRjlUQTYbCk5teRsdRVQcDhkDADBFHwhJ
+AQ8XClJBNl4AC1IdBghVEwARABoHCAdFXjwdGEkDCBMHBgAwW1YnUgAaRyon
+B0VTGgoZUwE7EhxNCAAFVAMXTjwaTSdSEAESUlQNBFJOZU5LXHQMHE0EF0EA
+Bh9FeRp5LQdFTkAZREgMU04CEFMcMQQAQ0lkay0ABwcqXwA1FwgFAk4dBkIA
+CA4aB0l0PD1MSQ8PEE87ADtbTmIGDAILAB0cRSo3ABwBRTYKFhROHUETCgZU
+MVQHYhoGGksABwdJAB0ASTpFNwQcTRoDBBgDUkksGioRHUkKCE5THEVCC08E
+EgF0BBwJSQoOGkgGADpfADETDU5tBzcJEFMLTx0bAHQJCx8ADRJUDRdMN1RH
+YgYGTi5jMURFeQEaSRAEOkURDAUCQRkKUmQ5XgBIKwYbQFIRSBVJGgwBGgtz
+RRNNDwcVWE8BT3hJVCcCSQwGQx9IBE4KTwwdASEXF01jIgQATwZIPRpXKwYK
+BkdEGwsRTxxDSToGMUlSCQZOFRwKUkQ5VEMnUh0BR0MBGgAAZDwGUwY7CBdN
+HB5BFwMdUz0aQSwWSQoITlMcRUILTxoCEDUXF01jNw4BTwVBNlRBYhAIGhNM
+EUgIRU5CRFMkOhwGBAQLTVQOHFkvUkUwF0lkbXkbHUVUBgAcFA0gRQYFCBpB
+PU8FQSsaVycTAkJHYhsRSQAXABxUFzFFFggICkEDHR1OPxoqER1JDQhNEUgK
+TkJPDAUAJhwQAg0XQRUBFgArU04lUh0GDlNUGwpOCU9jeTY1HFJARE4xGA4L
+ACxSQTZSDxsJSw1ICFUdBgpTNjUcXk0OAUEDBxtUPRpCLQtFTgBPVB8NSRoK
+SREKLUUVAklkERgOCwAsUkE2Ug8bCUsNSAhVHQYKUyI7RQUFABoEVA0dWXQa
+Ry1SHgYOVBFIB08XQ0kUCnRvPgwQTgUbGBwAOVREYhAGAQBJEUgETgpPGR8E
+LUUGBQgaQRIaHEshGk03AQANR1QdBAkAFwAcUwE9AFxNY2QxGA4LACxSQTZS
+DxsJSw1ICFUdBgpTJjsIF00GAE1ULB1NPRpPLF5JAgJUVAUAAAYKCAFFXjUe
+DBBOFRwOBgA+T04pC0kDElMdC0VXBgYdFkU2CgtNEAEUVBwTWXhTVG5SGg8e
+AB0cRSo+AwgKRSANExlJCBQaBAsANU9TKxFJL0dMHRwRTAtPBRwQMAAATQcB
+FlRlIkw5QwA2GggaR0YBBg5ZTgIcAAw3SVIaAQcVEU8QTyEaYy0fDE4ITlhI
+Jk8DCkkcC3hFMQIEC0EbAVIqCFZBO1IdBgZUVA4QTgUWSR4QJwwRTWM=
--- /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 base64
+import crapto
+
+# Check usage
+if args.length != 1 then
+ print "Usage: repeating_key_xor_solve <cipher_file>"
+ exit 1
+end
+
+# Read the cipher from the file
+var cipher_bytes = args[0].to_path.read_all_bytes.decode_base64
+
+# Create a RepeatingKeyXorCipher object to manipulate your ciphertext
+var xorcipher = new RepeatingKeyXorCipher
+xorcipher.ciphertext = cipher_bytes
+
+# Try to find the best candidate key
+xorcipher.find_key
+
+# Decrypt the cipher according to the found key
+xorcipher.decrypt
+
+# Check the resulting plaintext out...
+print xorcipher.plaintext
--- /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.
+
+# Cryptographic attacks and utilities for XOR-based algorithms.
+module xor
+
+import combinations
+import crypto
+import english_utils
+
+redef class SingleByteXorCipher
+ # Tries to find key using frequency analysis on all possible plaintexts.
+ # Populates `self.key`
+ fun find_key do
+
+ # Accumulate best result
+ var max = 0.0
+ var best = 0.to_b
+
+ # Iterate on possible values for a byte
+ var xor_b = new Bytes.with_capacity(1)
+ for b in [0 .. 255] do
+ # Need `Bytes` to pass to xor
+ xor_b[0] = b.to_b
+
+ # Xor and evaluate result
+ var xored = ciphertext.xorcipher(xor_b)
+ var result = xored.to_s.english_scoring
+ if result > max then
+ max = result
+ best = b.to_b
+ end
+ end
+
+ self.key = best
+
+ end
+end
+
+redef class RepeatingKeyXorCipher
+ # Attempts to find the key by using frequency analysis on the resulting plaintexts.
+ # Best key lengths are estimated using the hamming distance of blocks of keylength bytes.
+ # Ciphertext is then transposed in such a way that it can be solved by sequences of
+ # SingleByteXor (one for each offset in the key).
+ #
+ # `min_keylength` and `max_keylength` are limits as to what key lengths are tested.
+ # `considered_keylength_count` is the number of best key lengths that are kept to be
+ # analysed by the SingleByteXor frequency analysis.
+ fun find_key(min_keylength, max_keylength, considered_keylength_count: nullable Int) do
+
+ # Set default values
+ if min_keylength == null then min_keylength = 2
+ if max_keylength == null then max_keylength = 40
+ if considered_keylength_count == null then considered_keylength_count = 3
+
+ # Find the best candidate key lengths based on the normalized hamming distances
+ var best_sizes = get_normalized_hamming_distances(min_keylength, max_keylength, considered_keylength_count)
+
+ var best = 0.0
+ var best_key: nullable Bytes = null
+ for ks in best_sizes do
+
+ # Rearrange ciphertext to be in SingleByteXORable blocks
+ var transposed = transpose_ciphertext(ks)
+
+ var key = new Bytes.empty
+ for slot in transposed do
+ var sbx = new SingleByteXorCipher
+ sbx.ciphertext = slot
+ sbx.find_key
+ key.add sbx.key
+ end
+
+ # Evaluate the resulting plaintext based on english frequency analysis
+ var eng_score = ciphertext.xorcipher(key).to_s.english_scoring
+ if eng_score > best then
+ best_key = key
+ best = eng_score
+ end
+
+ assert best_key != null
+ self.key = best_key
+
+ end
+ end
+
+ # Computes the normalized hamming distances between blocks of ciphertext of length between `min_length` and `max_length`.
+ # The `considered_keylength_count` smallest results are returned
+ private fun get_normalized_hamming_distances(min_keylength, max_keylength, considered_keylength_count: Int): Array[Int] do
+
+ var normalized_distances = new HashMap[Float, Int]
+
+ # Iterate over all given key lengths
+ for ks in [min_keylength .. max_keylength[ do
+
+ # Initialize the blocks of size `ks`
+ var blocks = new Array[Bytes]
+ while (blocks.length + 1) * ks < ciphertext.length do
+ blocks.add(ciphertext.slice(blocks.length * ks, ks))
+ end
+
+ # Compute the normalized hamming distance with all block combinations
+ var pairs = new CombinationCollection[Bytes](blocks, 2)
+ var hamming_dists = new Array[Float]
+ for p in pairs do
+ hamming_dists.add(p[0].hamming_distance(p[1]).to_f / ks.to_f)
+ end
+
+ # Normalize the results based on the number of blocks
+ var normalized = 0.0
+ for dist in hamming_dists do normalized += dist
+ normalized /= hamming_dists.length.to_f
+ normalized_distances[normalized] = ks
+
+ end
+
+ # Collect the best candidates
+ var distances = normalized_distances.keys.to_a
+ default_comparator.sort(distances)
+ var best_distances = distances.subarray(0, considered_keylength_count)
+ var best_sizes = [for d in best_distances do normalized_distances[d]]
+
+ return best_sizes
+ end
+
+ # Returns a rearranged format of the ciphertext where every byte of a slot will be XORed with the same offset of a key of length `keylength`.
+ private fun transpose_ciphertext(keylength: Int): Array[Bytes] do
+ var transposed = new Array[Bytes]
+ for i in [0 .. keylength[ do
+ transposed.add(new Bytes.empty)
+ end
+
+ for byte_idx in [0 .. ciphertext.length[ do
+ transposed[byte_idx % keylength].add(ciphertext[byte_idx])
+ end
+
+ return transposed
+ end
+end
# See the License for the specific language governing permissions and
# limitations under the License.
-# Mix of all things cryptography-related
-module crypto
+# Basic cryptographic ciphers and utilities.
+module basic_ciphers
redef class Char
# Rotates self of `x`
end
redef class Bytes
-
- # Returns `self` xored with `key`
- #
- # The key is cycled through until the `self` has been completely xored.
- #
- # assert "goodmorning".to_bytes.xorcipher(" ".to_bytes) == "GOODMORNING".bytes
- fun xorcipher(key: Bytes): Bytes do
- var xored = new Bytes.with_capacity(self.length)
-
- for i in self.length.times do
- xored.add(self[i] ^ key[i % key.length])
+ # Computes the edit/hamming distance of two sequences of bytes.
+ #
+ # assert "this is a test".to_bytes.hamming_distance("wokka wokka!!!".bytes) == 37
+ # assert "this is a test".to_bytes.hamming_distance("this is a test".bytes) == 0
+ #
+ fun hamming_distance(other: SequenceRead[Byte]): Int do
+ var diff = 0
+ for idx in self.length.times do
+ var res_byte = self[idx] ^ other[idx]
+ for bit in [0..8[ do
+ if res_byte & 1u8 == 1u8 then diff += 1
+ res_byte = res_byte >> 1
+ end
end
-
- return xored
+ return diff
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.
+
+# Mix of all things cryptography-related
+module crypto
+
+import basic_ciphers
+import xor_ciphers
[upstream]
browse=https://github.com/nitlang/nit/tree/master/lib/crypto.nit
git=https://github.com/nitlang/nit.git
-git.directory=lib/crypto.nit
+git.directory=lib/crypto/crypto.nit
homepage=http://nitlanguage.org
issues=https://github.com/nitlang/nit/issues
--- /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.
+
+# XOR oriented cryptographic ciphers and utilities.
+module xor_ciphers
+
+redef class Bytes
+ # Returns `self` xored with `key`
+ #
+ # The key is cycled through until the `self` has been completely xored.
+ #
+ # assert "goodmorning".to_bytes.xorcipher(" ".to_bytes) == "GOODMORNING".bytes
+ fun xorcipher(key: Bytes): Bytes do
+ var xored = new Bytes.with_capacity(self.length)
+
+ for i in self.length.times do
+ xored.add(self[i] ^ key[i % key.length])
+ end
+
+ return xored
+ end
+end
+
+# Base class to modelize cryptographic ciphers
+abstract class Cipher
+
+ # Encrypted data
+ var ciphertext = new Bytes.empty is writable
+
+ # Unencrypted data
+ var plaintext = new Bytes.empty is writable
+
+ # Encrypt plaintext and populate `self.ciphertext`
+ fun encrypt is abstract
+
+ # Decrypt ciphertext and populate `self.plaintext`
+ fun decrypt is abstract
+
+end
+
+# Simple XOR cipher where the whole plaintext is XORed with a single byte.
+class SingleByteXorCipher
+ super Cipher
+
+ # Cryptographic key used in encryption and decryption.
+ var key: Byte = 0.to_b
+
+ redef fun encrypt do
+ var key_bytes = new Bytes.with_capacity(1)
+ key_bytes.add(key)
+ ciphertext = plaintext.xorcipher(key_bytes)
+ end
+
+ redef fun decrypt do
+ var key_bytes = new Bytes.with_capacity(1)
+ key_bytes.add(key)
+ plaintext = ciphertext.xorcipher(key_bytes)
+ end
+end
+
+# XOR cipher where the key is repeated to match the length of the message.
+class RepeatingKeyXorCipher
+ super Cipher
+
+ # Cryptographic key used in encryption and decryption.
+ var key = new Bytes.empty
+
+ redef fun encrypt do
+ assert key.length > 0
+ ciphertext = plaintext.xorcipher(key)
+ end
+
+ redef fun decrypt do
+ assert key.length > 0
+ plaintext = ciphertext.xorcipher(key)
+ end
+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.
android_manifest """<uses-feature android:glEsVersion="0x00020000"/>"""
end
-import ::android
+import ::android::game
intrude import android::load_image
private import gamnit::egl
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.0
# Check and sleep to maintain a frame-rate bellow `maximum_fps`
#
# Is automatically called at the end of `full_frame`.
fun limit_fps
do
- var t = clock.total.sec
+ var t = clock.total
if t >= frame_count_deadline then
var cfps = frame_count.to_f / 5.0
self.current_fps = cfps
frame_count = 0
- frame_count_deadline = t + 5
+ frame_count_deadline = t + 5.0
end
frame_count += 1
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
# Handles serialization and deserialization of objects to/from JSON
#
-# ## Nity JSON
+# ## Writing JSON with metadata
#
# `JsonSerializer` write Nit objects that subclass `Serializable` to JSON,
-# and `JsonDeserializer` can read them. They both use meta-data added to the
+# and `JsonDeserializer` can read them. They both use metadata added to the
# generated JSON to recreate the Nit instances with the exact original type.
#
# For more information on Nit serialization, see: ../serialization/README.md
#
-# ## Plain JSON
+# ## Writing plain JSON
#
# The attribute `JsonSerializer::plain_json` triggers generating plain and
# clean JSON. This format is easier to read for an human and a non-Nit program,
# but it cannot be fully deserialized. It can still be read by services from
# `json::static` and `json::dynamic`.
#
-# A shortcut to this service is provided by `Serializable::to_plain_json`.
+# A shortcut to these writing services is provided by `Serializable::serialize_to_json`.
#
# ### Usage Example
#
# var bob = new Person("Bob", 1986)
# var alice = new Person("Alice", 1978, bob)
#
-# assert bob.to_plain_json == """
-# {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}"""
+# assert bob.serialize_to_json(pretty=true, plain=true) == """
+#{
+# "name": "Bob",
+# "year_of_birth": 1986,
+# "next_of_kin": null
+#}"""
#
-# assert alice.to_plain_json == """
-# {"name": "Alice", "year_of_birth": 1978, "next_of_kin": {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}}"""
+# assert alice.serialize_to_json(pretty=true, plain=true) == """
+#{
+# "name": "Alice",
+# "year_of_birth": 1978,
+# "next_of_kin": {
+# "name": "Bob",
+# "year_of_birth": 1986,
+# "next_of_kin": null
+# }
+#}"""
# ~~~
#
# ## JSON to Nit objects
#
-# The `JsonDeserializer` support reading JSON code with minimal meta-data
+# The `JsonDeserializer` support reading JSON code with minimal metadata
# to easily create Nit object from client-side code or configuration files.
# Each JSON object must define the `__class` attribute with the corresponding
# Nit class and the expected attributes with its name in Nit followed by its value.
# var deserializer = new JsonDeserializer(json_code)
#
# var meet = deserializer.deserialize
+#
+# # Check for errors
+# assert deserializer.errors.is_empty
+#
# assert meet isa MeetupConfig
# assert meet.description == "My Awesome Meetup"
# assert meet.max_participants == null
import ::serialization::caching
private import ::serialization::engine_tools
-import static
+private import static
+private import string_parser
# Serializer of Nit objects to Json string.
class JsonSerializer
#
# If `false`, the default, serialize to support deserialization:
#
- # * Write meta-data, including the types of the serialized objects so they can
+ # * Write metadata, including the types of the serialized objects so they can
# be deserialized to their original form using `JsonDeserializer`.
# * Use references when an object has already been serialized so to not duplicate it.
# * Support cycles in references.
# * Preserve the Nit `Char` type as an object because it does not exist in JSON.
# * The generated JSON is standard and can be read by non-Nit programs.
# However, some Nit types are not represented by the simplest possible JSON representation.
- # With the added meta-data, it can be complex to read.
+ # With the added metadata, it can be complex to read.
#
# If `true`, serialize for other programs:
#
# * Nit objects are serialized for every references, so they can be duplicated.
# It is easier to read but it creates a larger output.
# * Does not support cycles, will replace the problematic references by `null`.
- # * Does not serialize the meta-data needed to deserialize the objects
+ # * Does not serialize the metadata needed to deserialize the objects
# back to regular Nit objects.
# * Keys of Nit `HashMap` are converted to their string representation using `to_s`.
var plain_json = false is writable
end
first_attribute = true
- object.serialize_to_json self
+ object.accept_json_serializer self
first_attribute = false
if plain_json then open_objects.pop
private var text: Text
# Root json object parsed from input text.
- private var root: nullable Jsonable is noinit
+ private var root: nullable Object is noinit
# Depth-first path in the serialized object tree.
- private var path = new Array[JsonObject]
+ private var path = new Array[Map[String, nullable Object]]
# Last encountered object reference id.
#
init do
var root = text.parse_json
- if root isa JsonObject then path.add(root)
+ if root isa Map[String, nullable Object] then path.add(root)
self.root = root
end
return null
end
- if object isa JsonObject then
+ if object isa Map[String, nullable Object] then
var kind = null
if object.keys.has("__kind") then
kind = object["__kind"]
# deserialized = deserializer.deserialize
# assert deserialized isa MyError
# ~~~
- protected fun class_name_heuristic(json_object: JsonObject): nullable String
+ protected fun class_name_heuristic(json_object: Map[String, nullable Object]): nullable String
do
return null
end
return res
end
- redef fun serialize_to_json(v) do v.stream.write(to_json)
+ redef fun accept_json_serializer(v) do v.stream.write(to_json)
end
redef class Serializable
- private fun serialize_to_json(v: JsonSerializer)
+
+ # Serialize `self` to JSON
+ #
+ # Set `plain = true` to generate standard JSON, without deserialization metadata.
+ # Use this option if the generated JSON will be read by other programs or humans.
+ # Use the default, `plain = false`, if the JSON is to be deserialized by a Nit program.
+ #
+ # Set `pretty = true` to generate pretty JSON for human eyes.
+ # Use the default, `pretty = false`, to generate minified JSON.
+ #
+ # This method should not be refined by subclasses,
+ # instead `accept_json_serializer` can customize the serialization of an object.
+ #
+ # See: `JsonSerializer`
+ fun serialize_to_json(plain, pretty: nullable Bool): String
+ do
+ var stream = new StringWriter
+ var serializer = new JsonSerializer(stream)
+ serializer.plain_json = plain or else false
+ serializer.pretty_json = pretty or else false
+ serializer.serialize self
+ stream.close
+ return stream.to_s
+ end
+
+ # Refinable service to customize the serialization of this class to JSON
+ #
+ # This method can be refined to customize the serialization by either
+ # writing pure JSON directly on the stream `v.stream` or
+ # by using other services of `JsonSerializer`.
+ #
+ # Most of the time, it is preferable to refine the method `core_serialize_to`
+ # which is used by all the serialization engines, not just JSON.
+ protected fun accept_json_serializer(v: JsonSerializer)
do
var id = v.cache.new_id_for(self)
v.stream.write "\{"
v.new_line_and_indent
v.stream.write "\}"
end
-
- # Serialize this object to a JSON string with metadata for deserialization
- fun to_json_string: String
- do
- var stream = new StringWriter
- var serializer = new JsonSerializer(stream)
- serializer.serialize self
- stream.close
- return stream.to_s
- end
-
- # Serialize this object to plain JSON
- #
- # This is a shortcut using `JsonSerializer::plain_json`,
- # see its documentation for more information.
- fun to_plain_json: String
- do
- var stream = new StringWriter
- var serializer = new JsonSerializer(stream)
- serializer.plain_json = true
- serializer.serialize self
- stream.close
- return stream.to_s
- end
end
redef class Int
- redef fun serialize_to_json(v) do v.stream.write(to_s)
+ redef fun accept_json_serializer(v) do v.stream.write to_s
end
redef class Float
- redef fun serialize_to_json(v) do v.stream.write(to_s)
+ redef fun accept_json_serializer(v) do v.stream.write to_s
end
redef class Bool
- redef fun serialize_to_json(v) do v.stream.write(to_s)
+ redef fun accept_json_serializer(v) do v.stream.write to_s
end
redef class Char
- redef fun serialize_to_json(v)
+ redef fun accept_json_serializer(v)
do
if v.plain_json then
- v.stream.write to_s.to_json
+ to_s.accept_json_serializer v
else
v.stream.write "\{\"__kind\": \"char\", \"__val\": "
- v.stream.write to_s.to_json
+ to_s.accept_json_serializer v
v.stream.write "\}"
end
end
end
redef class NativeString
- redef fun serialize_to_json(v) do to_s.serialize_to_json(v)
+ redef fun accept_json_serializer(v) do to_s.accept_json_serializer(v)
end
redef class Collection[E]
end
redef class SimpleCollection[E]
- redef fun serialize_to_json(v)
+ redef fun accept_json_serializer(v)
do
# Register as pseudo object
if not v.plain_json then
end
redef class Map[K, V]
- redef fun serialize_to_json(v)
+ redef fun accept_json_serializer(v)
do
# Register as pseudo object
var id = v.cache.new_id_for(self)
v.new_line_and_indent
var k = key or else "null"
- v.stream.write k.to_s.to_json
+ k.to_s.accept_json_serializer v
v.stream.write ": "
if not v.try_to_serialize(val) then
assert val != null # null would have been serialized
interface Jsonable
# Encode `self` in JSON.
#
+ # This is a recursive method which can be refined by any subclasses.
+ # To write any `Serializable` object to JSON, see `serialize_to_json`.
+ #
# SEE: `append_json`
fun to_json: String is abstract
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
import mnit
import mnit::opengles1
-import ::android
+import ::android::game
intrude import ::android::input_events
in "C" `{
private var frame_count = 0
# Deadline used to compute `current_fps`
- private var frame_count_deadline = 0
+ private var frame_count_deadline = 0.0
# Check and sleep to maitain a frame-rate bellow `maximum_fps`
# Also periodically uptate `current_fps`
# Is automatically called at the end of `full_frame`.
fun limit_fps
do
- var t = clock.total.sec
+ var t = clock.total
if t >= frame_count_deadline then
var cfps = frame_count.to_f / 5.0
self.current_fps = cfps
frame_count = 0
- frame_count_deadline = t + 5
+ frame_count_deadline = t + 5.0
end
frame_count += 1
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
+
+ # Default file returned when no static file matches the requested URI.
+ #
+ # If no `default_file` is provided, the FileServer responds 404 error to
+ # unmatched queries.
+ var default_file: nullable String = null is writable
+
redef fun answer(request, turi)
do
var response
# This make sure that the requested file is within the root folder.
if (local_file + "/").has_prefix(root) then
# Does it exists?
- if local_file.file_exists then
- if local_file.file_stat.is_dir then
+ var file_stat = local_file.file_stat
+ if file_stat != null then
+ if file_stat.is_dir then
# If we target a directory without an ending `/`,
# redirect to the directory ending with `/`.
- if not request.uri.is_empty and
- request.uri.chars.last != '/' then
- response = new HttpResponse(303)
- response.header["Location"] = request.uri + "/"
- return response
+ var uri = request.uri
+ if not uri.is_empty and uri.chars.last != '/' then
+ return answer_redirection(request.uri + "/")
end
# Show index file instead of the directory listing
end
end
- response = new HttpResponse(200)
- if local_file.file_stat.is_dir then
- # Show the directory listing
- var title = turi
- var files = local_file.files
+ file_stat = local_file.file_stat
+ if show_directory_listing and file_stat != null and file_stat.is_dir then
+ response = answer_directory_listing(request, turi, local_file)
+ else if file_stat != null and not file_stat.is_dir then # It's a single file
+ response = answer_file(local_file)
+ else response = answer_default
+ else response = answer_default
+ else response = new HttpResponse(403)
- alpha_comparator.sort files
+ if response.status_code != 200 then
+ var tmpl = error_page(response.status_code)
+ if header != null and tmpl isa ErrorTemplate then tmpl.header = header
+ response.body = tmpl.to_s
+ end
- var links = new Array[String]
- if turi.length > 1 then
- var path = (request.uri + "/..").simplify_path
- links.add "<a href=\"{path}/\">..</a>"
- end
- for file in files do
- var local_path = local_file.join_path(file).simplify_path
- var web_path = file.simplify_path
- if local_path.file_stat.is_dir then web_path = web_path + "/"
- links.add "<a href=\"{web_path}\">{file}</a>"
- end
+ return response
+ end
+
+ # Answer the `default_file` if any.
+ fun answer_default: HttpResponse do
+ var default_file = self.default_file
+ if default_file == null then
+ return new HttpResponse(404)
+ end
+
+ var local_file = (root / default_file).simplify_path
+ return answer_file(local_file)
+ end
- var header = self.header
- var header_code
- if header != null then
- header_code = header.write_to_string
- else header_code = ""
+ # Answer a 303 redirection to `location`.
+ fun answer_redirection(location: String): HttpResponse do
+ var response = new HttpResponse(303)
+ response.header["Location"] = location
+ return response
+ end
+
+ # Build a reponse containing a single `local_file`.
+ #
+ # Returns a 404 error if local_file does not exists.
+ fun answer_file(local_file: String): HttpResponse do
+ if not local_file.file_exists then return new HttpResponse(404)
+
+ var response = new HttpResponse(200)
+ response.files.add local_file
+
+ # Set Content-Type depending on the file extension
+ var ext = local_file.file_extension
+ if ext != null then
+ var media_type = media_types[ext]
+ if media_type != null then
+ response.header["Content-Type"] = media_type
+ else response.header["Content-Type"] = "application/octet-stream"
+ end
- response.body = """
+ # Cache control
+ response.header["cache-control"] = cache_control
+ return response
+ end
+
+ # Answer with a directory listing for files within `local_files`.
+ fun answer_directory_listing(request: HttpRequest, turi, local_file: String): HttpResponse do
+ # Show the directory listing
+ var title = turi
+ var files = local_file.files
+
+ alpha_comparator.sort files
+
+ var links = new Array[String]
+ if turi.length > 1 then
+ var path = (request.uri + "/..").simplify_path
+ links.add "<a href=\"{path}/\">..</a>"
+ end
+ for file in files do
+ var local_path = local_file.join_path(file).simplify_path
+ var web_path = file.simplify_path
+ var file_stat = local_path.file_stat
+ if file_stat != null and file_stat.is_dir then web_path = web_path + "/"
+ links.add "<a href=\"{web_path}\">{file}</a>"
+ end
+
+ var header = self.header
+ var header_code
+ if header != null then
+ header_code = header.write_to_string
+ else header_code = ""
+
+ var response = new HttpResponse(200)
+ response.body = """
<!DOCTYPE html>
<head>
<meta charset="utf-8">
</body>
</html>"""
- response.header["Content-Type"] = media_types["html"].as(not null)
- else
- # It's a single file
- response.files.add local_file
-
- var ext = local_file.file_extension
- if ext != null then
- var media_type = media_types[ext]
- if media_type != null then
- response.header["Content-Type"] = media_type
- else response.header["Content-Type"] = "application/octet-stream"
- end
-
- # Cache control
- response.header["cache-control"] = cache_control
- end
-
- else response = new HttpResponse(404)
- else response = new HttpResponse(403)
-
- if response.status_code != 200 then
- var tmpl = error_page(response.status_code)
- if header != null and tmpl isa ErrorTemplate then tmpl.header = header
- response.body = tmpl.to_s
- end
-
+ response.header["Content-Type"] = media_types["html"].as(not null)
return response
end
end
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"
if val == null then return null
var deserializer = new JsonDeserializer(val)
+ var obj = deserializer.deserialize
+
if deserializer.errors.not_empty then
print_error deserializer.errors.join("\n")
return null
end
- return deserializer.deserialize
+ return obj
end
end
redef class HttpRequest
# The `Session` associated to this request
- var session: nullable Session = null
+ var session: nullable Session = null is writable
end
redef class HttpRequestParser
# 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
# Total execution time of this event
var sum = 0.0
- # Register a new event execution time with a `Timespec`
- fun add(lapse: Timespec) do add_float lapse.to_f
-
- # Register a new event execution time in seconds using a `Float`
- fun add_float(time: Float)
+ # Register a new event execution time in seconds
+ fun add(time: Float)
do
if time.to_f < min.to_f or count == 0 then min = time
if time.to_f > max.to_f then max = time
--- /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.
+
+In some cases, you can want to redirect request to static files to a default file
+instead of returning a 404 error.
+This can be achieved by specifying a default file in the StaticHandler:
+
+~~~
+app.use("/static/", new StaticHandler("public/", "default.html"))
+~~~
+
+This way all non-matched queries to the StaticHandler will be answered with the
+`default.html` file.
+
+## 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.
+
+## Response cycle
+
+When the popcorn `App` receives a request, the response cycle is the following:
+
+1. `pre-middlewares` lookup matching middlewares registered with `use_before(pre_middleware)`:
+ 1. execute matching middleware by registration order
+ 2. if a middleware send a response then let the `pre-middlewares` loop continue
+ with the next middleware
+2. `response-handlers` lookup matching handlers registered with `use(handler)`:
+ 1. execute matching middleware by registration order
+ 2. if a middleware send a response then stop the `response-handlers` loop
+ 3. if no hander matches or sends a response, generate a 404 response
+3. `post-middlewares` lookup matching handlers registered with `use_after(post_handler)`:
+ 1. execute matching middleware by registration order
+ 2. if a middleware send a response then let the `post-middlewares` loop continue
+ with the next middleware
+
+## 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_before("/*", 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.
+
+Be default, the order of middleware execution is that are loaded first are also executed first.
+To ensure our middleware `MyLogger` will be executed before all the other, we add it
+with the `use_before` method.
+
+### 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}s)"
+ 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_before("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use_after("/*", 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.
+Because of the `use_before` method, the `RequestTimeHandler` middleware will be executed
+before all the others.
+
+We then let the `HelloHandler` produce the response.
+
+Finally, our `LogHandler` will display a bunch of data and use the request `timer`
+to display the time it took to process the request.
+Because of the `use_after` method, the `LogHandler` middleware will be executed after
+all the others.
+
+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/", "index.html"))
+app.listen("localhost", 3000)
+~~~
+
+Because the StaticHandler will not find the angular routes as static files,
+you must specify the path to the default angular controller.
+In this example, the StaticHandler will redirect any unknown requests to the `index.html`
+angular controller.
+
+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/", "index.html"))
+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}s)"
+ 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_before("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use_after("/*", 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)
# This file is part of NIT ( http://www.nitlanguage.org ).
#
-# Copyright 2012 Alexis Laferrière <alexis.laf@xymus.net>
+# 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.
# See the License for the specific language governing permissions and
# limitations under the License.
-import realtime
+import popcorn
-redef extern class Timespec
- fun simplify : Int
- do
- return sec*1000000 + nanosec/1000
+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
-var c = new Clock
-var t0 = c.total.simplify
-
-print "sleeping 1s"
-nanosleep(1, 0)
-print c.total.sec >= 1
-print c.lapse.sec >= 1
+class HelloHandler
+ super Handler
-var t1 = c.total.simplify
-
-print "sleeping 5000ns"
-nanosleep(0, 5000)
-print c.lapse.nanosec >= 5000
+ redef fun get(req, res) do res.send "Hello World!"
+end
-var t2 = c.total.simplify
-print t0 <= t1
-print t1 <= t2
+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_before("/*", 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/", "default.html"))
+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>Default Page</h1>
+ </body>
+</html>
--- /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
+
+ # Default file to serve if nothing matches the request.
+ #
+ # `null` for no default file.
+ var default_file: nullable 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
+ srv.default_file = default_file
+ 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]
+
+ # List of handlers to match before every other.
+ private var pre_handlers = new Map[AppRoute, Handler]
+
+ # List of handlers to match after every other.
+ private var post_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 = build_route(handler, path)
+ handlers[route] = handler
+ end
+
+ # Register a pre-handler for a route `path`.
+ #
+ # Prehandlers are matched before every other handlers in registrastion order.
+ fun use_before(path: String, handler: Handler) do
+ var route = build_route(handler, path)
+ pre_handlers[route] = handler
+ end
+
+ # Register a post-handler for a route `path`.
+ #
+ # Posthandlers are matched after every other handlers in registrastion order.
+ fun use_after(path: String, handler: Handler) do
+ var route = build_route(handler, path)
+ post_handlers[route] = handler
+ end
+
+ redef fun handle(route, uri, req, res) do
+ if not route.match(uri) then return
+ handle_pre(route, uri, req, res)
+ handle_in(route, uri, req, res)
+ handle_post(route, uri, req, res)
+ end
+
+ private fun handle_pre(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+ for hroute, handler in pre_handlers do
+ handler.handle(hroute, route.uri_root(uri), req, res)
+ end
+ end
+
+ private fun handle_in(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+ for hroute, handler in handlers do
+ handler.handle(hroute, route.uri_root(uri), req, res)
+ if res.sent then break
+ end
+ end
+
+ private fun handle_post(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+ for hroute, handler in post_handlers do
+ handler.handle(hroute, route.uri_root(uri), req, res)
+ end
+ end
+
+ private fun build_route(handler: Handler, path: String): AppRoute do
+ if handler isa Router or handler isa StaticHandler then
+ return new AppGlobRoute(path)
+ else if path.has_suffix("*") then
+ return new AppGlobRoute(path)
+ else
+ return new AppParamRoute(path)
+ 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}s)"
+ 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 pre_handlers do
+ handler.handle(route, uri, req, res)
+ end
+ for route, handler in handlers do
+ handler.handle(route, uri, req, res)
+ if res.sent then break
+ end
+ if not res.sent then
+ res.send(error_tpl(res.status_code, res.status_message), 404)
+ end
+ for route, handler in post_handlers do
+ handler.handle(route, uri, req, res)
+ 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}
+[Client] curl -s localhost:*****/not_found
+<!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
+
+[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:*****/
+<!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>Default Page</h1>
+ </body>
+</html>
+
+[Client] curl -s localhost:*****/css/not_found.nit
+<!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>Default Page</h1>
+ </body>
+</html>
+
+[Client] curl -s localhost:*****/static/css/not_found.nit
+<!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>Default Page</h1>
+ </body>
+</html>
+
+[Client] curl -s localhost:*****/not_found.nit
+<!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>Default Page</h1>
+ </body>
+</html>
--- /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>
+ <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_before("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use_after("/*", 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"
+ system "curl -s {host}:{port}/not_found" # handled by angular controller
+ return null
+ end
+end
+
+var app = new App
+app.use("/counter", new CounterAPI)
+app.use("/*", new StaticHandler("../examples/angular/www/", "index.html"))
+
+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_before("/*", 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_default
+
+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/", "default.html"))
+
+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"
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015-2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+#
+# 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.
+
+# A native wrapper ove the postgres c api
+module native_postgres is pkgconfig("libpq")
+
+in "C header" `{
+ #include <libpq-fe.h>
+`}
+
+extern class ExecStatusType `{int`}
+ new empty `{ return PGRES_EMPTY_QUERY; `}
+ new command_ok `{ return PGRES_COMMAND_OK; `}
+ new tuples_ok `{ return PGRES_TUPLES_OK; `}
+ new copy_out `{ return PGRES_COPY_OUT; `}
+ new copy_in `{ return PGRES_COPY_IN; `}
+ new bad_response `{ return PGRES_BAD_RESPONSE; `}
+ new nonfatal_error `{ return PGRES_NONFATAL_ERROR; `}
+ new fatal_error `{ return PGRES_FATAL_ERROR; `}
+
+ fun is_ok: Bool `{
+ return !(self == PGRES_BAD_RESPONSE || self == PGRES_NONFATAL_ERROR || self == PGRES_FATAL_ERROR);
+ `}
+
+ redef fun to_s import NativeString.to_s `{
+ char * err = PQresStatus(self);
+ if(err == NULL) err = "";
+ return NativeString_to_s(err);
+ `}
+end
+
+extern class ConnStatusType `{int`}
+ new connection_ok `{ return CONNECTION_OK; `}
+ new connection_bad `{ return CONNECTION_BAD; `}
+
+ fun is_ok: Bool `{return self == CONNECTION_OK; `}
+end
+
+extern class NativePGResult `{PGresult *`}
+ # Frees the memory block associated with the result
+ fun clear `{PQclear(self); `}
+
+ # Returns the number of rows in the query result
+ fun ntuples:Int `{ return PQntuples(self); `}
+
+ # Returns the number of columns in each row of the query result
+ fun nfields:Int `{return PQnfields(self); `}
+
+ # Returns the ExecStatusType of a result
+ fun status: ExecStatusType `{ return PQresultStatus(self); `}
+
+ # Returns the field name of a given column_number
+ fun fname(column_number:Int):String import NativeString.to_s `{
+ return NativeString_to_s( PQfname(self, column_number));
+ `}
+
+ # Returns the column number associated with the column name
+ fun fnumber(column_name:String):Int import String.to_cstring `{
+ return PQfnumber(self, String_to_cstring(column_name));
+ `}
+
+ # Returns a single field value of one row of the result at row_number, column_number
+ fun value(row_number:Int, column_number:Int):String import NativeString.to_s `{
+ return NativeString_to_s(PQgetvalue(self, row_number, column_number));
+ `}
+
+ # Tests wether a field is a null value
+ fun is_null(row_number:Int, column_number: Int): Bool `{
+ return PQgetisnull(self, row_number, column_number);
+ `}
+
+end
+extern class NativePostgres `{PGconn *`}
+
+ # Connect to a new database using the conninfo string as a parameter
+ new connectdb(conninfo: Text) import Text.to_cstring `{
+ PGconn * self = NULL;
+ self = PQconnectdb(Text_to_cstring(conninfo));
+ return self;
+ `}
+
+ # Submits a query to the server and waits for the result returns the ExecStatustype of the query
+ fun exec(query: Text): NativePGResult import Text.to_cstring `{
+ PGresult *res = PQexec(self, Text_to_cstring(query));
+ return res;
+ `}
+
+ # Prepares a statement with the given parameters
+ fun prepare(stmt: String, query: String, nParams: Int): NativePGResult import String.to_cstring `{
+ const char * stmtName = String_to_cstring(stmt);
+ const char * queryStr = String_to_cstring(query);
+ PGresult * res = PQprepare(self, stmtName, queryStr, nParams, NULL);
+ return res;
+ `}
+
+ fun exec_prepared(stmt: String, nParams: Int, values: Array[String], pLengths: Array[Int], pFormats: Array[Int], resultFormat: Int): NativePGResult import String.to_cstring, Array[String].[], Array[Int].[] `{
+ const char * stmtName = String_to_cstring(stmt);
+ const char * paramValues[nParams];
+ int paramLengths[nParams];
+ int paramFormats[nParams];
+ int i;
+ for(i = 0; i < nParams; i++)
+ paramValues[i] = String_to_cstring(Array_of_String__index(values, i));
+ for(i = 0; i < nParams; i++)
+ paramLengths[i] = Array_of_Int__index(pLengths, i);
+ for(i = 0; i < nParams; i++)
+ paramFormats[i] = Array_of_Int__index(pFormats, i);
+ PGresult * res = PQexecPrepared(self, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat);
+ return res;
+ `}
+
+ # Returns the error message of the last operation on the connection
+ fun error: String import NativeString.to_s `{
+ char * error = PQerrorMessage(self);
+ return NativeString_to_s(error);
+ `}
+
+ # Returns the status of this connection
+ fun status: ConnStatusType `{
+ return PQstatus(self);
+ `}
+
+ # Closes the connection to the server
+ fun finish `{
+ PQfinish(self);
+ `}
+
+ # Closes the connection to the server and attempts to reconnect with the previously used params
+ fun reset `{
+ PQreset(self);
+ `}
+end
--- /dev/null
+[package]
+name=postgresql
+tags=database,lib
+maintainer=Guilherme Mansur <guilhermerpmansur@gmail.com>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/postgresql/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/postgresql/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
\ No newline at end of file
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Guilherme Mansur<guilhermerpmansur@gmail.com>
+#
+# 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.
+
+# Services to manipulate a Postgres database
+#
+# For more information, refer to the documentation of http://www.postgresql.org/docs/manuals/
+#
+# ### Usage example
+#
+# ~~~
+# class Animal
+# var name: String
+# var kind: String
+# var age: Int
+# end
+#
+# var animals = new Array[Animal]
+# var dog = new Animal("Lassy", "dog", 10)
+# var cat = new Animal("Garfield", "cat", 3)
+# var turtle = new Animal("George", "turtle", 123)
+#
+# animals.add(dog)
+# animals.add(cat)
+# animals.add(turtle)
+#
+# var db = new Postgres.open("dbname=postgres")
+#
+# assert db_is_open: not db.is_closed
+# assert create_table: db.create_table("IF NOT EXISTS animals (aname TEXT PRIMARY KEY, kind TEXT NOT NULL, age INT NOT NULL)") else print db.error
+#
+# for animal in animals do
+# assert insert: db.insert("INTO animals VALUES('{animal.name}', '{animal.kind}', {animal.age})") else print db.error
+# end
+#
+# var result = db.raw_execute("SELECT * FROM animals")
+# assert result.is_ok
+# assert drop_table: db.execute("DROP TABLE animals")
+# db.finish
+# assert db_is_closed: db.is_closed
+# ~~~
+module postgres
+
+private import native_postgres
+
+# A connection to a Postgres database
+class Postgres
+ private var native_connection: NativePostgres
+
+ var is_closed = true
+
+ # Open the connnection with the database using the `conninfo`
+ init open(conninfo: Text)
+ do
+ init(new NativePostgres.connectdb(conninfo))
+ if native_connection.status.is_ok then is_closed = false
+ end
+
+ # Close this connection with the database
+ fun finish
+ do
+ if is_closed then return
+
+ is_closed = true
+
+ native_connection.finish
+ end
+
+ fun prepare(stmt_name:String, query:String, num_params: Int):PGResult do return new PGResult(native_connection.prepare(stmt_name, query, num_params))
+
+ fun exec_prepared(stmt_name: String, num_params: Int, values: Array[String], param_lengths: Array[Int], param_formats: Array[Int], result_format: Int):PGResult do
+ return new PGResult(native_connection.exec_prepared(stmt_name, num_params, values, param_lengths, param_formats, result_format))
+ end
+
+ # Executes a `query` and returns the raw `PGResult`
+ fun raw_execute(query: Text): PGResult do return new PGResult(native_connection.exec(query))
+
+ # Execute the `sql` statement and returns `true` on success
+ fun execute(query: Text): Bool do return native_connection.exec(query).status.is_ok
+
+ # Create a table on the DB with a statement beginning with "CREATE TABLE ", followed by `rest`
+ #
+ # This method does not escape special characters.
+ fun create_table(rest: Text): Bool do return execute("CREATE TABLE " + rest)
+
+ # Insert in the DB with a statement beginning with "INSERT ", followed by `rest`
+ #
+ # This method does not escape special characters.
+ fun insert(rest: Text): Bool do return execute("INSERT " + rest)
+
+ # Replace in the DB with a statement beginning with "REPLACE", followed by `rest`
+ #
+ # This method does not escape special characters.
+ fun replace(rest: Text): Bool do return execute("REPLACE " + rest)
+
+ # The latest error message on the connection an empty string if none
+ fun error: String do return native_connection.error
+
+ # The status of this connection
+ fun is_valid: Bool do return native_connection.status.is_ok
+
+ # Resets the connection to the database
+ fun reset do native_connection.reset
+end
+
+# The result of a given query
+class PGResult
+ private var pg_result: NativePGResult
+
+ fun clear do pg_result.clear
+
+ # Returns the number of rows in the query result
+ fun ntuples:Int do return pg_result.ntuples
+
+ # Returns the number of columns in each row of the query result
+ fun nfields:Int do return pg_result.nfields
+
+ # Returns the ExecStatusType of a result
+ fun is_ok:Bool do return pg_result.status.is_ok
+
+ # Returns the field name of a given `column_number`
+ fun fname(column_number:Int):String do return pg_result.fname(column_number)
+
+ # Returns the column number associated with the `column_name`
+ fun fnumber(column_name:String):Int do return pg_result.fnumber(column_name)
+
+ # Returns a single field value of one row of the result at `row_number`, `column_number`
+ fun value(row_number:Int, column_number:Int):String do return pg_result.value(row_number, column_number)
+
+ # Tests wether a field specified by the `row_number` and `column_number` is null.
+ fun is_null(row_number:Int, column_number: Int): Bool do return pg_result.is_null(row_number, column_number)
+end
clock_gettime(CLOCK_MONOTONIC, self);
`}
- # Substract a Timespec from `self`.
- fun - ( o : Timespec ) : Timespec
+ # Subtract `other` from `self`
+ fun -(other: Timespec): Timespec
do
- var s = sec - o.sec
- var ns = nanosec - o.nanosec
- if ns > nanosec then s += 1
- return new Timespec( s, ns )
+ var s = sec - other.sec
+ var ns = nanosec - other.nanosec
+ if ns < 0 then
+ s -= 1
+ ns += 1000000000
+ end
+ return new Timespec(s, ns)
end
# Number of whole seconds of elapsed time.
end
# Keeps track of real time
+#
+# ~~~
+# var clock = new Clock
+#
+# # sleeping at least 1s
+# 1.0.sleep
+# assert clock.total >= 1.0
+# assert clock.lapse >= 1.0
+#
+# # sleeping at least 5ms
+# 0.005.sleep
+# assert clock.total >= 1.005
+# assert clock.lapse >= 0.005
+# ~~~
class Clock
- # Time at instanciation
+ super FinalizableOnce
+
+ # TODO use less mallocs
+
+ # Time at creation
protected var time_at_beginning = new Timespec.monotonic_now
# Time at last time a lapse method was called
protected var time_at_last_lapse = new Timespec.monotonic_now
# Smallest time frame reported by clock
- fun resolution : Timespec `{
+ fun resolution: Timespec `{
struct timespec* tv = malloc( sizeof(struct timespec) );
#ifdef __MACH__
clock_serv_t cclock;
return tv;
`}
- # Return timelapse since instanciation of this instance
- fun total : Timespec
+ # Seconds since the creation of this instance
+ fun total: Float
do
- return new Timespec.monotonic_now - time_at_beginning
+ var now = new Timespec.monotonic_now
+ var diff = now - time_at_beginning
+ var r = diff.to_f
+ diff.free
+ now.free
+ return r
end
- # Return timelapse since last call to lapse
- fun lapse : Timespec
+ # Seconds since the last call to `lapse`
+ fun lapse: Float
do
var nt = new Timespec.monotonic_now
var dt = nt - time_at_last_lapse
+ var r = dt.to_f
+ dt.free
+ time_at_last_lapse.free
time_at_last_lapse = nt
- return dt
+ return r
+ end
+
+ # Seconds since the last call to `lapse`, without resetting the lapse counter
+ fun peek_lapse: Float
+ do
+ var nt = new Timespec.monotonic_now
+ var dt = nt - time_at_last_lapse
+ var r = dt.to_f
+ nt.free
+ dt.free
+ return r
+ end
+
+ redef fun finalize_once
+ do
+ time_at_beginning.free
+ time_at_last_lapse.free
end
end
[package]
name=rubix
tags=algo,lib
-maintainer=Lucas Bajolet<lucas.bajolet@hotmail.com>
+maintainer=Lucas Bajolet<r4pass@hotmail.com>
license=Apache-2.0
[upstream]
browse=https://github.com/nitlang/nit/tree/master/lib/rubix.nit
# 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)
#include <sqlite3.h>
`}
+in "C" `{
+ // Return code of the last call to the constructor of `NativeSqlite3`
+ static int nit_sqlite_open_error = SQLITE_OK;
+`}
+
redef class Sys
# Last error raised when calling `Sqlite3::open`
- var sqlite_open_error: nullable Sqlite3Code = null
+ fun sqlite_open_error: Sqlite3Code `{ return nit_sqlite_open_error; `}
end
extern class Sqlite3Code `{int`}
private fun native_to_s: NativeString `{
#if SQLITE_VERSION_NUMBER >= 3007015
- char *err = (char *)sqlite3_errstr(self);
+ return (char *)sqlite3_errstr(self);
#else
- char *err = "sqlite3_errstr supported only by version >= 3.7.15";
+ return "sqlite3_errstr is not supported in version < 3.7.15";
#endif
- return err;
`}
end
return sqlite3_step(self);
`}
- fun column_name(i: Int) : String import NativeString.to_s `{
- const char * name = (sqlite3_column_name(self, i));
- if(name == NULL){
- name = "";
- }
- char * ret = (char *) name;
- return NativeString_to_s(ret);
+ fun column_name(i: Int): NativeString `{
+ return (char *)sqlite3_column_name(self, i);
`}
# Number of bytes in the blob or string at row `i`
extern class NativeSqlite3 `{sqlite3 *`}
# Open a connection to a database in UTF-8
- new open(filename: NativeString) import set_sys_sqlite_open_error `{
+ new open(filename: NativeString) `{
sqlite3 *self = NULL;
int err = sqlite3_open(filename, &self);
- NativeSqlite3_set_sys_sqlite_open_error(self, (void*)(long)err);
- // The previous cast is a hack, using non pointers in extern classes is not
- // yet in the spec of the FFI.
+ nit_sqlite_open_error = err;
return self;
`}
- # Utility method to set `Sys.sqlite_open_error`
- private fun set_sys_sqlite_open_error(err: Sqlite3Code) do sys.sqlite_open_error = err
-
# Has this DB been correctly opened?
#
# To know if it has been closed or interrupted, you must check for errors with `error`.
fun is_valid: Bool do return not address_is_null
- fun destroy do close
-
# Close this connection
fun close `{
#if SQLITE_VERSION_NUMBER >= 3007014
`}
# Execute a SQL statement
- fun exec(sql: String): Sqlite3Code import String.to_cstring `{
- return sqlite3_exec(self, String_to_cstring(sql), 0, 0, 0);
+ fun exec(sql: NativeString): Sqlite3Code `{
+ return sqlite3_exec(self, sql, NULL, NULL, NULL);
`}
# Prepare a SQL statement
- fun prepare(sql: String): nullable NativeStatement import String.to_cstring, NativeStatement.as nullable `{
+ fun prepare(sql: NativeString): NativeStatement `{
sqlite3_stmt *stmt;
- int res = sqlite3_prepare_v2(self, String_to_cstring(sql), -1, &stmt, 0);
+ int res = sqlite3_prepare_v2(self, sql, -1, &stmt, 0);
if (res == SQLITE_OK)
- return NativeStatement_as_nullable(stmt);
+ return stmt;
else
- return null_NativeStatement();
+ return NULL;
`}
fun last_insert_rowid: Int `{
# Prepare and return a `Statement`, return `null` on error
fun prepare(sql: Text): nullable Statement
do
- var native_stmt = native_connection.prepare(sql.to_s)
- if native_stmt == null then return null
+ var native_stmt = native_connection.prepare(sql.to_cstring)
+ if native_stmt.address_is_null then return null
var stmt = new Statement(native_stmt)
open_statements.add stmt
# Execute the `sql` statement and return `true` on success
fun execute(sql: Text): Bool
do
- var err = native_connection.exec(sql.to_s)
+ var err = native_connection.exec(sql.to_cstring)
return err.is_ok
end
do
if not native_connection.is_valid then
var err = sys.sqlite_open_error
- if err == null then return null
+ if err.is_ok then return null
return err.to_s
end
var name: String is lazy do
assert statement_closed: statement.is_open
- return statement.native_statement.column_name(index)
+ var cname = statement.native_statement.column_name(index)
+ assert not cname.address_is_null
+ return cname.to_s
end
# Get the value of this entry according to its Sqlite type
redef universal Float super Sqlite3Data end
-redef class String
- super Sqlite3Data
+redef class String super Sqlite3Data end
+
+redef class Text
# Return `self` between `'`s, escaping `\` and `'`
#
do
return "'{self.replace('\\', "\\\\").replace('\'', "''")}'"
end
+
+ # Format the date represented by `self` into an escaped string for SQLite
+ #
+ # `self` must be composed of 1 to 3 integers separated by '-'.
+ # An incompatible format will result in an invalid date string.
+ #
+ # assert "2016-5-1".to_sql_date_string == "'2016-05-01'"
+ # assert "2016".to_sql_date_string == "'2016-01-01'"
+ fun to_sql_date_string: String
+ do
+ var parts = self.split("-")
+ for i in [parts.length .. 3[ do parts[i] = "1"
+
+ var year = parts[0].justify(4, 1.0, '0')
+ var month = parts[1].justify(2, 1.0, '0')
+ var day = parts[2].justify(2, 1.0, '0')
+ return "{year}-{month}-{day}".to_sql_string
+ end
end
# A Sqlite3 blob
# 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.
#
--- /dev/null
+# This is a basic install of Nit on a debian base.
+
+FROM debian:jessie
+MAINTAINER Jean Privat <jean@pryen.org>
+
+# Install dependencies
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ # Recomanded builds pakages
+ build-essential \
+ ccache \
+ libgc-dev \
+ graphviz \
+ libunwind-dev \
+ pkg-config \
+ # Get the code!
+ git \
+ ca-certificates \
+ curl \
+ # For nit manpages :)
+ man \
+ && rm -rf /var/lib/apt/lists/*
+
+# Clone and compile
+RUN git clone https://github.com/nitlang/nit.git /root/nit \
+ && cd /root/nit \
+ && make \
+ && . misc/nit_env.sh install \
+ # Clean and reduce size
+ && strip c_src/nitc bin/nit* \
+ && ccache -C \
+ && rm -rf .git
+
+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"
## CUSTOMIZATION
-### `--sharedir`
-Directory containing nitdoc assets.
+### `--share-dir`
+Directory containing tools assets.
By default `$NIT_DIR/share/nitdoc/` is used.
$ nitunit foo.md
+When testing, the environment variable `NIT_TESTING` is set to `true`.
+This flag can be used by libraries and program to prevent (or limit) the execution of dangerous pieces of code.
+
+~~~~~
+# NIT_TESTING is automatically set.
+#
+# assert "NIT_TESTING".environ == "true"
+~~~~
+
## Working with `TestSuites`
TestSuites are Nit files that define a set of TestCases for a particular module.
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`.
+
+### `NIT_TESTING`
+
+The environment variable `NIT_TESTING` is set to `true` during the execution of program tests.
+Some libraries of programs can use it to produce specific reproducible results; or just to exit their executions.
+
+Unit-tests may unset this environment variable to retrieve the original behavior of such piece of software.
+
+### `SRAND`
+
+In order to maximize reproducibility, `SRAND` is set to 0.
+This make the pseudo-random generator no random at all.
+See `Sys::srand` for details.
+
+To retrieve the randomness, unit-tests may unset this environment variable then call `srand`.
+
+### `NIT_TESTING_ID`
+
+Parallel executions can cause some race collisions on named resources (e.g. DB table names).
+To solve this issue, `NIT_TESTING_ID` is initialized with a distinct integer identifier that can be used to give unique names to resources.
+
+Note: `rand` is not a recommended way to get a distinct identifier because its randomness is disabled by default. See `SRAND`.
+
+
# SEE ALSO
The Nit language documentation and the source code of its tools and libraries may be downloaded from <http://nitlanguage.org>
# Number of line of code by package
var loc = new Counter[MPackage]
+ # Number of errors
+ var errors = new Counter[MPackage]
+
+ # 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]
+
# Number of commits by package
var commits = 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
var mclasses = 0
var mmethods = 0
var loc = 0
+ var errors = 0
+ var warnings = 0
+ # The documentation value of each entity is ad hoc.
+ var entity_score = 0.0
+ var doc_score = 0.0
for g in mpackage.mgroups do
mmodules += g.mmodules.length
+ entity_score += 1.0
+ if g.mdoc != null then doc_score += 1.0
for m in g.mmodules do
+ var source = m.location.file
+ if source != null then
+ for msg in source.messages do
+ if msg.level == 2 then
+ errors += 1
+ else
+ warnings += 1
+ end
+ end
+ end
var am = modelbuilder.mmodule2node(m)
if am != null then
var file = am.location.file
loc += file.line_starts.length - 1
end
end
+ entity_score += 1.0
+ if m.mdoc != null then doc_score += 1.0
for cd in m.mclassdefs do
+ var s = 0.2
+ if not cd.is_intro then s /= 10.0
+ if not cd.mclass.visibility <= private_visibility then s /= 10.0
+ entity_score += s
+ if cd.mdoc != null then doc_score += s
mclasses += 1
for pd in cd.mpropdefs do
+ s = 0.1
+ if not pd.is_intro then s /= 10.0
+ if not pd.mproperty.visibility <= private_visibility then s /= 10.0
+ entity_score += s
+ if pd.mdoc != null then doc_score += s
if not pd isa MMethodDef then continue
mmethods += 1
end
self.mclasses[mpackage] = mclasses
self.mmethods[mpackage] = mmethods
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
#score += mmodules.score
score += mclasses.score
score += mmethods.score
score += loc.score
+ score += documentation_score.score
self.score[mpackage] = score.to_i
end
var opt_source = new OptionString("Format to link source code (%f for filename, " +
"%l for first line, %L for last line)", "--source")
- # Directory where the CSS and JS is stored.
- var opt_sharedir = new OptionString("Directory containing nitdoc assets", "--sharedir")
-
# Use a shareurl instead of copy shared files.
#
# This is usefull if you don't want to store the Nitdoc templates with your
super
option_context.add_option(
- opt_source, opt_sharedir, opt_shareurl, opt_custom_title,
+ opt_source, opt_share_dir, opt_shareurl, opt_custom_title,
opt_custom_footer, opt_custom_intro, opt_custom_brand,
opt_github_upstream, opt_github_base_sha1, opt_github_gitdir,
opt_piwik_tracker, opt_piwik_site_id,
var output_dir = ctx.output_dir
if not output_dir.file_exists then output_dir.mkdir
# locate share dir
- var sharedir = ctx.opt_sharedir.value
- if sharedir == null then
- var dir = ctx.nit_dir
- sharedir = dir/"share/nitdoc"
- if not sharedir.file_exists then
- print "Error: cannot locate nitdoc share files. Uses --sharedir or envvar NIT_DIR"
- abort
- end
- end
+ var sharedir = ctx.share_dir / "nitdoc"
# copy shared files
if ctx.opt_shareurl.value == null then
sys.system("cp -r -- {sharedir.to_s.escape_to_sh}/* {output_dir.to_s.escape_to_sh}/")
srcs.add_all mmodule.ffi_files
# Compiler options specific to this module
- var ldflags = mmodule.ldflags[""].join(" ")
+ var ldflags_array = mmodule.ldflags[""]
+ if ldflags_array.has("-lrt") and system("sh -c 'uname -s 2>/dev/null || echo not' | grep Darwin >/dev/null") == 0 then
+ # Remove -lrt on OS X
+ ldflags_array.remove "-lrt"
+ end
+ var ldflags = ldflags_array.join(" ")
# Protect pkg-config
var pkgconfigs = mmodule.pkgconfigs
var keep = new Array[String]
var res = new Array[String]
for a in args do
+ var stat = a.to_path.stat
+ if stat != null and stat.is_dir then
+ res.add a
+ continue
+ end
var l = identify_module(a)
if l == null then
keep.add a
import ordered_tree
private import more_collections
+redef class MEntity
+ # The visibility of the MEntity.
+ #
+ # MPackages, MGroups and MModules are always public.
+ # The visibility of `MClass` and `MProperty` is defined by the keyword used.
+ # `MClassDef` and `MPropDef` return the visibility of `MClass` and `MProperty`.
+ fun visibility: MVisibility do return public_visibility
+end
+
redef class Model
# All known classes
var mclasses = new Array[MClass]
# The visibility of the class
# In Nit, the visibility of a class cannot evolve in refinements
- var visibility: MVisibility
+ redef var visibility
init
do
# Is `self` and abstract class?
var is_abstract: Bool is lazy do return kind == abstract_kind
+
+ redef fun mdoc_or_fallback do return intro.mdoc_or_fallback
end
redef var location: Location
+ redef fun visibility do return mclass.visibility
+
# Internal name combining the module and the class
# Example: "mymodule$MyClass"
redef var to_s is noinit
redef var location
+ redef fun mdoc_or_fallback do return intro.mdoc_or_fallback
+
# The canonical name of the property.
#
# It is currently the short-`name` prefixed by the short-name of the class and the full-name of the module.
end
# The visibility of the property
- var visibility: MVisibility
+ redef var visibility
# Is the property usable as an initializer?
var is_autoinit = false is writable
redef var location: Location
+ redef fun visibility do return mproperty.visibility
+
init
do
mclassdef.mpropdefs.add(self)
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]
var mmodules = new HashSet[MModule]
mmodules.add self
mmodules.add_all collect_ancestors(view)
- mmodules.add_all collect_parents(view)
- mmodules.add_all collect_children(view)
mmodules.add_all collect_descendants(view)
return view.mmodules_poset(mmodules)
end
redef class MClass
+ redef fun collect_modifiers do return intro.collect_modifiers
+
# Collect direct parents of `self` with `visibility >= to min_visibility`.
fun collect_parents(view: ModelView): Set[MClass] do
var res = new HashSet[MClass]
return res
end
+ # Build a class hierarchy poset for `self` based on its ancestors and descendants.
+ fun hierarchy_poset(mainmodule: MModule, view: ModelView): POSet[MClass] do
+ var mclasses = new HashSet[MClass]
+ mclasses.add self
+ mclasses.add_all collect_ancestors(view)
+ mclasses.add_all collect_descendants(view)
+ return view.mclasses_poset(mainmodule, mclasses)
+ end
+
# Collect all mproperties introduced in 'self' with `visibility >= min_visibility`.
fun collect_intro_mproperties(view: ModelView): Set[MProperty] do
var set = new HashSet[MProperty]
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
+ obj["visibility"] = visibility
+ obj["location"] = location
+ 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
+ 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["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["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
+ 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["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["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["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.
# See the specific implementation in the subclasses.
fun visit_all(v: ModelVisitor) do end
- private fun accept_visibility(min_visibility: nullable MVisibility): Bool do return true
+ private fun accept_visibility(min_visibility: nullable MVisibility): Bool do
+ if min_visibility == null then return true
+ return visibility >= min_visibility
+ end
end
redef class Model
end
end
-redef class MClass
- redef fun accept_visibility(min_visibility) do
- if min_visibility == null then return true
- return visibility >= min_visibility
- end
-end
-
redef class MClassDef
# Visit all the classes and class definitions of the module.
#
v.enter_visit(x)
end
end
-
- redef fun accept_visibility(min_visibility) do
- if min_visibility == null then return true
- return mclass.visibility >= min_visibility
- end
-end
-
-redef class MProperty
- redef fun accept_visibility(min_visibility) do
- if min_visibility == null then return true
- return visibility >= min_visibility
- end
-end
-
-redef class MPropDef
- redef fun accept_visibility(min_visibility) do
- if min_visibility == null then return true
- return mproperty.visibility >= min_visibility
- end
end
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
end
res.add "</ul>\n"
+ res.add "<h3>Quality</h3>\n<ul class=\"box\">\n"
+ var errors = errors[mpackage]
+ if errors > 0 then
+ res.add "<li>{errors} errors</li>\n"
+ end
+ 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 "<h3>Tags</h3>\n"
var ts2 = new Array[String]
for t in mpackage.tags do
res.add "<th data-field=\"met\" data-sortable=\"true\">methods</th>\n"
res.add "<th data-field=\"loc\" data-sortable=\"true\">lines</th>\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 "<tr>"
res.add "<td>{mmethods[p]}</td>"
res.add "<td>{loc[p]}</td>"
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
res.add "</table>\n"
var mmodules = modelbuilder.parse_full(args)
modelbuilder.run_phases
-if opt_full.value then mmodules = model.mmodules
+if opt_full.value then mmodules = modelbuilder.parsed_modules
var dir = opt_dir.value
if dir != null then
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)
exit(0)
end
+"NIT_TESTING".setenv("true")
+"NIT_TESTING_ID".setenv(pid.to_s)
+"SRAND".setenv("0")
+
+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
+import console
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
+
+ # Show a single-line status to use as a progression.
+ #
+ # Note that the line starts with `'\r'` and is not ended by a `'\n'`.
+ # So it is expected that:
+ # * no other output is printed between two calls
+ # * the last `show_unit_status` is followed by a new-line
+ fun show_unit_status(name: String, tests: SequenceRead[UnitTest], more_message: nullable String)
+ do
+ var esc = 27.code_point.to_s
+ var line = "\r{esc}[K* {name} ["
+ var done = tests.length
+ for t in tests do
+ if not t.is_done then
+ line += " "
+ done -= 1
+ else if t.error == null then
+ line += ".".green.bold
+ else
+ line += "X".red.bold
+ end
+ end
+ line += "] {done}/{tests.length}"
+ if more_message != null then
+ line += " " + more_message
+ end
+ printn "{line}"
+ end
+
+end
+
+# A unit test is an elementary test discovered, run and reported by nitunit.
+#
+# This class factorizes `DocUnit` and `TestCase`.
+abstract class UnitTest
+ # The name of the unit to show in messages
+ fun full_name: String is abstract
+
+ # The location of the unit test to show in messages.
+ fun location: Location is abstract
+
+ # Flag that indicates if the unit test was compiled/run.
+ var is_done: Bool = false is writable
+
+ # Error message occurred during test-case execution (or compilation).
+ #
+ # e.g.: `Runtime Error`
+ var error: nullable String = null is writable
+
+ # Was the test case executed at least once?
+ #
+ # This will indicate the status of the test (failture or error)
+ var was_exec = false is writable
+
+ # The raw output of the execution (or compilation)
+ #
+ # It merges the standard output and error output
+ var raw_output: nullable String = null is writable
+
+ # The location where the error occurred, if it makes sense.
+ var error_location: nullable Location = null is writable
+
+ # A colorful `[OK]` or `[KO]`.
+ fun status_tag: String do
+ if not is_done then
+ return "[ ]"
+ else if error != null then
+ return "[KO]".red.bold
+ else
+ return "[OK]".green.bold
+ end
+ end
+
+ # The full (color) description of the test-case.
+ #
+ # `more message`, if any, is added after the error message.
+ fun to_screen(more_message: nullable String): String do
+ var res
+ var error = self.error
+ if error != null then
+ if more_message != null then error += " " + more_message
+ var loc = error_location or else location
+ res = "{status_tag} {full_name}\n {loc.to_s.yellow}: {error}\n{loc.colored_line("1;31")}"
+ var output = self.raw_output
+ if output != null then
+ res += "\n Output\n\t{output.chomp.replace("\n", "\n\t")}\n"
+ end
+ else
+ res = "{status_tag} {full_name}"
+ if more_message != null then res += more_message
+ end
+ return res
+ end
+
+ # Return a `<testcase>` XML node in format compatible with Jenkins unit tests.
+ 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(error)
+ else
+ tc.open("failure").append(error)
+ end
+ end
+ var output = self.raw_output
+ if output != null then
+ tc.open("system-err").append(output.trunc(8192).filter_nonprintable)
+ end
+ return tc
+ end
+
+ # The `classname` attribute of the XML format.
+ #
+ # NOTE: jenkins expects a '.' in the classname attr
+ #
+ # See to_xml
+ 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]
+ # The name of the suite
+ var name: 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
# Populate `blocks` from the markdown decorator
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
var docunits = new Array[DocUnit]
+ fun show_status(more_message: nullable String)
+ do
+ toolcontext.show_unit_status(name, docunits, more_message)
+ end
+
+ fun mark_done(du: DocUnit)
+ do
+ du.is_done = true
+ show_status(du.full_name + " " + du.status_tag)
+ end
+
# Execute all the docunits
fun run_tests
do
+ if docunits.is_empty then
+ return
+ end
+
var simple_du = new Array[DocUnit]
+ show_status
for du in docunits do
+ # Skip existing errors
+ if du.error != null then
+ mark_done(du)
+ continue
+ end
+
var ast = toolcontext.parse_something(du.block)
if ast isa AExpr then
simple_du.add du
end
test_simple_docunits(simple_du)
+
+ show_status
+ print ""
+
+ for du in docunits do
+ print du.to_screen
+ end
+
+ 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")
+ du.was_exec = true
- n2 = new HTMLTag("system-out")
- tc.add n2
- n2.append(du.block)
+ var content = "{file}.out1".to_path.read_all
+ du.raw_output = content
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 = "Runtime error in {file} with argument {i}"
toolcontext.modelbuilder.failed_entities += 1
end
- toolcontext.check_errors
-
- testsuite.add(tc)
+ mark_done(du)
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")
+ du.was_exec = true
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
+ du.raw_output = content
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 = "Compilation error in {file}"
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}")
+ du.error = "Runtime error in {file}"
toolcontext.modelbuilder.failed_entities += 1
end
- toolcontext.check_errors
-
- testsuite.add(tc)
+ mark_done(du)
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
# Try to parse code blocks
var ast = executor.toolcontext.parse_something(code)
+ var mdoc = executor.mdoc
+ assert mdoc != null
+
# Skip pure comments
if ast isa TComment then return
+ # The location is computed according to the starts of the mdoc and the block
+ # Note, the following assumes that all the comments of the mdoc are correctly aligned.
+ var loc = block.block.location
+ var line_offset = loc.line_start + mdoc.location.line_start - 2
+ var column_offset = loc.column_start + mdoc.location.column_start
+ # Hack to handle precise location in blocks
+ # TODO remove when markdown is more reliable
+ if block isa BlockFence then
+ # Skip the starting fence
+ line_offset += 1
+ else
+ # Account a standard 4 space indentation
+ column_offset += 4
+ end
+
# We want executable code
if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
- var message = ""
- if ast isa AError then message = " At {ast.location}: {ast.message}."
- executor.toolcontext.warning(executor.mdoc.location, "invalid-block", "Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).{message}")
- executor.failures.add("{executor.mdoc.location}: Invalid block of code.{message}")
+ var message
+ var l = ast.location
+ # Get real location of the node (or error)
+ var location = new Location(mdoc.location.file,
+ l.line_start + line_offset,
+ l.line_end + line_offset,
+ l.column_start + column_offset,
+ l.column_end + column_offset)
+ if ast isa AError then
+ message = ast.message
+ else
+ message = "Error: Invalid Nit code."
+ end
+
+ var du = new_docunit
+ du.block += code
+ du.error_location = location
+ du.error = message
+ executor.toolcontext.modelbuilder.failed_entities += 1
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
+
+ redef fun full_name do
+ var mentity = mdoc.original_mentity
+ if mentity != null then
+ return mentity.full_name
+ else
+ return xml_classname + "." + xml_name
+ end
+ 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.
+ redef var 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
var prefix = toolcontext.test_dir
prefix = prefix.join_path(mmodule.to_s)
- var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
-
- var tc
+ var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of module {mmodule.full_name}")
do
total_entities += 1
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
var prefix = toolcontext.test_dir
prefix = prefix.join_path(mgroup.to_s)
- var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
-
- var tc
+ var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of group {mgroup.full_name}")
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
ts.attr("package", file)
var prefix = toolcontext.test_dir / "file"
- var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
-
- var tc
+ var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts, "Docunits of file {file}")
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
# Test to be executed after the whole test suite.
var after_module: nullable TestCase = null
+ fun show_status(more_message: nullable String)
+ do
+ toolcontext.show_unit_status("Test-suite of module " + mmodule.full_name, test_cases, more_message)
+ end
+
# Execute the test suite
fun run do
+ show_status
if not toolcontext.test_dir.file_exists then
toolcontext.test_dir.mkdir
end
toolcontext.info("Execute test-suite {mmodule.name}", 1)
var before_module = self.before_module
if not before_module == null then before_module.run
- for case in test_cases do case.run
+ for case in test_cases do
+ case.run
+ show_status(case.full_name + " " + case.status_tag)
+ end
+
+ show_status
+ print ""
+
var after_module = self.after_module
if not after_module == null then after_module.run
+ for case in test_cases do
+ print case.to_screen
+ end
end
# Write the test unit for `self` in a nit compilable file.
# 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
# Test method to be compiled and tested.
var test_method: MMethodDef
+ redef fun full_name do return test_method.full_name
+
+ redef fun location do return test_method.location
+
# `ToolContext` to use to display messages and find `nitc` bin.
var toolcontext: ToolContext
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 f = new FileReader.open("{res_name}.out1")
- var msg = f.read_all
- f.close
+ var res = toolcontext.safe_exec("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
+ self.raw_output = "{res_name}.out1".to_path.read_all
# set test case result
- var loc = test_method.location
if res != 0 then
- error = msg
- toolcontext.warning(loc, "failure",
- "ERROR: {method_name} (in file {test_file}.nit): {msg}")
+ error = "Runtime Error in file {test_file}.nit"
toolcontext.modelbuilder.failed_tests += 1
+ else
+ # no error, check with res file, if any.
+ 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
+ self.raw_output = "Diff\n" + "{res_name}.diff".to_path.read_all
+ error = "Difference with expected output: diff -u {sav} {res_name}.out1"
+ toolcontext.modelbuilder.failed_tests += 1
+ end
+ else
+ toolcontext.info("No diff: {sav} not found", 2)
+ end
+ end
end
- toolcontext.check_errors
+ is_done = true
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
# * enclose identifiers, keywords and pieces of code with back-quotes.
var text: String
+ # The severity level
+ #
+ # * 0 is advices (see `ToolContext::advice`)
+ # * 1 is warnings (see `ToolContext::warning`)
+ # * 2 is errors (see `ToolContext::error`)
+ var level: Int
+
# Comparisons are made on message locations.
redef fun <(other: OTHER): Bool do
if location == null then return true
messages = ms
end
ms.add m
+ var s = file
+ if s != null then s.messages.add m
end
end
+redef class SourceFile
+ # Errors and warnings associated to the whole source.
+ var messages = new Array[Message]
+end
+
# Global context for tools
class ToolContext
# Number of errors
# Return the message (to add information)
fun error(l: nullable Location, s: String): Message
do
- var m = new Message(l,null,s)
+ var m = new Message(l, null, s, 2)
if messages.has(m) then return m
if l != null then l.add_message m
messages.add m
# Return the message (to add information) or null if the warning is disabled
fun warning(l: nullable Location, tag: String, text: String): nullable Message
do
- if opt_warning.value.has("no-{tag}") then return null
- if not opt_warning.value.has(tag) and opt_warn.value == 0 then return null
if is_warning_blacklisted(l, tag) then return null
- var m = new Message(l, tag, text)
+ var m = new Message(l, tag, text, 1)
if messages.has(m) then return null
if l != null then l.add_message m
+ if opt_warning.value.has("no-{tag}") then return null
+ if not opt_warning.value.has(tag) and opt_warn.value == 0 then return null
messages.add m
warning_count = warning_count + 1
if opt_stop_on_first_error.value then check_errors
# Return the message (to add information) or null if the warning is disabled
fun advice(l: nullable Location, tag: String, text: String): nullable Message
do
- if opt_warning.value.has("no-{tag}") then return null
- if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return null
if is_warning_blacklisted(l, tag) then return null
- var m = new Message(l, tag, text)
+ var m = new Message(l, tag, text, 0)
if messages.has(m) then return null
if l != null then l.add_message m
+ if opt_warning.value.has("no-{tag}") then return null
+ if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return null
messages.add m
warning_count = warning_count + 1
if opt_stop_on_first_error.value then check_errors
# Option --nit-dir
var opt_nit_dir = new OptionString("Base directory of the Nit installation", "--nit-dir")
+ # Option --share-dir
+ var opt_share_dir = new OptionString("Directory containing tools assets", "--share-dir")
+
# Option --help
var opt_help = new OptionBool("Show Help (This screen)", "-h", "-?", "--help")
# The identified root directory of the Nit package
var nit_dir: String is noinit
+ # Shared files directory.
+ #
+ # Most often `nit/share/`.
+ var share_dir: String is lazy do
+ var sharedir = opt_share_dir.value
+ if sharedir == null then
+ sharedir = nit_dir / "share"
+ if not sharedir.file_exists then
+ fatal_error(null, "Fatal Error: cannot locate shared files directory in {sharedir}. Uses --share-dir to define it's location.")
+ end
+ end
+ return sharedir
+ end
+
private fun compute_nit_dir: String
do
# the option has precedence
# * 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/
test_rubix_cube
test_rubix_visual
test_csv
+repeating_key_xor_solve
test_rubix_visual
test_rubix_cube
test_csv
+repeating_key_xor_solve
--- /dev/null
+../lib/crapto/examples/repeating_key_xor_cipher.txt
--- /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.
<li><a href="https://github.com/nitlang/nit/tree/master/tests/test_prog">https://github.com/nitlang/nit/tree/master/tests/test_prog</a></li>
<li><tt>https://github.com/nitlang/nit.git</tt></li>
</ul>
+<h3>Quality</h3>
+<ul class="box">
+<li>28 warnings (63/kloc)</li>
+<li>93% documented</li>
+</ul>
<h3>Tags</h3>
<a href="../index.html#tag_test">test</a>, <a href="../index.html#tag_game">game</a><h3>Requirements</h3>
none<h3>Clients</h3>
</span></span><span class="line" id="L33">
</span><span class="nc_cdef foldable" id="base_simple3$B"><span class="line" id="L34"><span class="nc_k">class</span> <span class="nc_t">B</span>
</span><span class="nc_pdef foldable" id="base_simple3$B$_val"><a id="base_simple3$B$val"></a><a id="base_simple3$B$val="></a><span class="line" id="L35"> <span class="nc_k">var</span> <span class="nc_def nc_i">val</span><span>:</span> <span class="nc_t">Int</span>
-</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36"> <span class="nc_k">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
+</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36"> <span class="nc_k popupable" style="border-bottom: solid 2px red" title="Messages" data-content="<div><div class="dropdown"> <a data-toggle="dropdown" href="#"><b>1 message(s)</b> <span class="caret"></span></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li>Warning: init with signature in base_simple3$B</li></ul></div></div>" data-toggle="popover">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
</span><span class="line" id="L37"> <span class="nc_k">do</span>
</span><span class="line" id="L38"> <span class="nc_l">7</span><span>.</span><span class="nc_i">output</span>
</span><span class="line" id="L39"> <span class="nc_k">self</span><span>.</span><span class="nc_i">val</span> <span>=</span> <span class="nc_v nc_i">v</span>
-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)
+\r\e[K* Docunits of module test_nitunit::test_nitunit [ ] 0/4\r\e[K* Docunits of module test_nitunit::test_nitunit [ \e[1m\e[31mX\e[m\e[m] 1/4 test_nitunit$X$foo1 \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of module test_nitunit::test_nitunit [\e[1m\e[32m.\e[m\e[m \e[1m\e[31mX\e[m\e[m] 2/4 test_nitunit::test_nitunit \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_nitunit::test_nitunit [\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m \e[1m\e[31mX\e[m\e[m] 3/4 test_nitunit$X \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of module test_nitunit::test_nitunit [\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m] 4/4 test_nitunit$X$foo \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of module test_nitunit::test_nitunit [\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m] 4/4
+\e[1m\e[32m[OK]\e[m\e[m test_nitunit::test_nitunit
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit$X
+ \e[33mtest_nitunit.nit:21,7--22,0\e[m: Runtime error in .nitunit/test_nitunit-2.nit
+ # \e[1;31massert false\e[0m
+ ^
+ Output
+ 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`.
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit$X$foo
+ \e[33mtest_nitunit.nit:24,8--25,0\e[m: Compilation error in .nitunit/test_nitunit-3.nit
+ # \e[1;31massert undefined_identifier\e[0m
+ ^
+ Output
+ .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)
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit$X$foo1
+ \e[33mtest_nitunit.nit:28,15\e[m: Syntax Error: unexpected operator '!'.
+ # assert \e[1;31m!\e[0m@#$%^&*()
+ ^
+\r\e[K* Test-suite of module test_test_nitunit::test_test_nitunit [ ] 0/3\r\e[K* Test-suite of module test_test_nitunit::test_test_nitunit [\e[1m\e[32m.\e[m\e[m ] 1/3 test_test_nitunit$TestX$test_foo \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Test-suite of module test_test_nitunit::test_test_nitunit [\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m ] 2/3 test_test_nitunit$TestX$test_foo1 \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Test-suite of module test_test_nitunit::test_test_nitunit [\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[32m.\e[m\e[m] 3/3 test_test_nitunit$TestX$test_foo2 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Test-suite of module test_test_nitunit::test_test_nitunit [\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[32m.\e[m\e[m] 3/3
+\e[1m\e[32m[OK]\e[m\e[m test_test_nitunit$TestX$test_foo
+\e[1m\e[31m[KO]\e[m\e[m test_test_nitunit$TestX$test_foo1
+ \e[33mtest_test_nitunit.nit:36,2--40,4\e[m: Runtime Error in file .nitunit/gen_test_test_nitunit.nit
+ \e[1;31m# will fail\e[0m
+ ^
+ Output
+ Runtime error: Assert failed (test_test_nitunit.nit:39)
+\e[1m\e[32m[OK]\e[m\e[m test_test_nitunit$TestX$test_foo2
DocUnits:
-Entities: 27; Documented ones: 3; With nitunits: 3; Failures: 2
+Entities: 27; Documented ones: 4; With nitunits: 4; Failures: 3
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
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="<class>"><error>Runtime error in .nitunit/test_nitunit-2.nit</error><system-err>Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5)
+</system-err><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 in .nitunit/test_nitunit-3.nit</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><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo1"><failure>Syntax Error: unexpected operator '!'.</failure><system-out>assert !@#$%^&*()
+</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"><system-err></system-err></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><error>Runtime Error in file .nitunit/gen_test_test_nitunit.nit</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"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
+\r\e[K* Docunits of module test_nitunit2::test_nitunit2 [ ] 0/3\r\e[K* Docunits of module test_nitunit2::test_nitunit2 [\e[1m\e[32m.\e[m\e[m ] 1/3 test_nitunit2::test_nitunit2$core::Sys$foo1 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_nitunit2::test_nitunit2 [\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m ] 2/3 test_nitunit2::test_nitunit2$core::Sys$bar2 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_nitunit2::test_nitunit2 [\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m] 3/3 test_nitunit2::test_nitunit2$core::Sys$foo3 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_nitunit2::test_nitunit2 [\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m] 3/3
+\e[1m\e[32m[OK]\e[m\e[m test_nitunit2::test_nitunit2$core::Sys$foo1
+\e[1m\e[32m[OK]\e[m\e[m test_nitunit2::test_nitunit2$core::Sys$bar2
+\e[1m\e[32m[OK]\e[m\e[m test_nitunit2::test_nitunit2$core::Sys$foo3
DocUnits:
DocUnits Success
Entities: 4; Documented ones: 3; With nitunits: 3; Failures: 0
+\r\e[K* Docunits of module test_doc2::test_doc2 [ ] 0/3\r\e[K* Docunits of module test_doc2::test_doc2 [\e[1m\e[32m.\e[m\e[m ] 1/3 test_doc2::test_doc2$core::Sys$foo1 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_doc2::test_doc2 [\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m ] 2/3 test_doc2::test_doc2$core::Sys$foo2 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_doc2::test_doc2 [\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m] 3/3 test_doc2::test_doc2$core::Sys$foo3 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_doc2::test_doc2 [\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m\e[1m\e[32m.\e[m\e[m] 3/3
+\e[1m\e[32m[OK]\e[m\e[m test_doc2::test_doc2$core::Sys$foo1
+\e[1m\e[32m[OK]\e[m\e[m test_doc2::test_doc2$core::Sys$foo2
+\e[1m\e[32m[OK]\e[m\e[m test_doc2::test_doc2$core::Sys$foo3
DocUnits:
DocUnits Success
Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 0
-test_nitunit3/README.md:1,0--13,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,2--4: Syntax Error: unexpected malformed character '\]..
-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)
+\r\e[K* Docunits of group test_nitunit3> [ ] 0/2\r\e[K* Docunits of group test_nitunit3> [ \e[1m\e[31mX\e[m\e[m] 1/2 test_nitunit3> \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of group test_nitunit3> [\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m] 2/2 test_nitunit3> \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of group test_nitunit3> [\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m] 2/2
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit3>
+ \e[33mtest_nitunit3/README.md:4,2--15,0\e[m: Runtime error in .nitunit/test_nitunit3-0.nit with argument 1
+ ~\e[1;31m~\e[0m
+ ^
+ Output
+ Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit3>
+ \e[33mtest_nitunit3/README.md:7,3--5\e[m: Syntax Error: unexpected malformed character '\].
+ ~~\e[1;31m~
+;\e[0m
+ ^
+\r\e[K* Docunits of module test_nitunit3::test_nitunit3 [ ] 0/1\r\e[K* Docunits of module test_nitunit3::test_nitunit3 [\e[1m\e[32m.\e[m\e[m] 1/1 test_nitunit3::test_nitunit3 \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Docunits of module test_nitunit3::test_nitunit3 [\e[1m\e[32m.\e[m\e[m] 1/1
+\e[1m\e[32m[OK]\e[m\e[m test_nitunit3::test_nitunit3
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:1,0--13,0: Invalid block of code. At 1,2--4: 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>"><error>Runtime error in .nitunit/test_nitunit3-0.nit with argument 1</error><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>Syntax Error: unexpected malformed character '\].</failure><system-out>;'\][]
+</system-out></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></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)
+\r\e[K* Docunits of file test_nitunit_md.md:1,0--15,0 [ ] 0/1\r\e[K* Docunits of file test_nitunit_md.md:1,0--15,0 [\e[1m\e[31mX\e[m\e[m] 1/1 nitunit.<file>.test_nitunit_md.md:1,0--15,0 \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of file test_nitunit_md.md:1,0--15,0 [\e[1m\e[31mX\e[m\e[m] 1/1
+\e[1m\e[31m[KO]\e[m\e[m nitunit.<file>.test_nitunit_md.md:1,0--15,0
+ \e[33mtest_nitunit_md.md:4,2--16,0\e[m: Runtime error in .nitunit/file-0.nit with argument 1
+ ~\e[1;31m~\e[0m
+ ^
+ Output
+ 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"><error>Runtime error in .nitunit/file-0.nit with argument 1</error><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
-test_doc3.nit:15,1--18,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,3--9: Syntax Error: unexpected identifier 'garbage'..
-test_doc3.nit:20,1--25,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,2--8: Syntax Error: unexpected identifier 'garbage'..
-test_doc3.nit:27,1--32,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,2--8: Syntax Error: unexpected identifier 'garbage'..
+\r\e[K* Docunits of module test_doc3::test_doc3 [ ] 0/3\r\e[K* Docunits of module test_doc3::test_doc3 [\e[1m\e[31mX\e[m\e[m ] 1/3 test_doc3::test_doc3$core::Sys$foo1 \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of module test_doc3::test_doc3 [\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m ] 2/3 test_doc3::test_doc3$core::Sys$foo2 \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of module test_doc3::test_doc3 [\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m] 3/3 test_doc3::test_doc3$core::Sys$foo3 \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Docunits of module test_doc3::test_doc3 [\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m\e[1m\e[31mX\e[m\e[m] 3/3
+\e[1m\e[31m[KO]\e[m\e[m test_doc3::test_doc3$core::Sys$foo1
+ \e[33mtest_doc3.nit:17,9--15\e[m: Syntax Error: unexpected identifier 'garbage'.
+ # *\e[1;31mgarbage\e[0m*
+ ^
+\e[1m\e[31m[KO]\e[m\e[m test_doc3::test_doc3$core::Sys$foo2
+ \e[33mtest_doc3.nit:23,4--10\e[m: Syntax Error: unexpected identifier 'garbage'.
+ # *\e[1;31mgarbage\e[0m*
+ ^
+\e[1m\e[31m[KO]\e[m\e[m test_doc3::test_doc3$core::Sys$foo3
+ \e[33mtest_doc3.nit:30,4--10\e[m: Syntax Error: unexpected identifier 'garbage'.
+ # *\e[1;31mgarbage\e[0m*
+ ^
DocUnits:
Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 3
TestSuites:
No test cases found
Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure message="test_doc3.nit:15,1--18,0: Invalid block of code. At 1,3--9: 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:20,1--25,0: Invalid block of code. At 1,2--8: 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:27,1--32,0: Invalid block of code. At 1,2--8: 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>Syntax Error: unexpected identifier 'garbage'.</failure><system-out> *garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure>Syntax Error: unexpected identifier 'garbage'.</failure><system-out>*garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure>Syntax Error: unexpected identifier 'garbage'.</failure><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
-Tested method
-After Test
-Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31)
+\r\e[K* Test-suite of module test_nitunit4::test_nitunit4 [ ] 0/3\r\e[K* Test-suite of module test_nitunit4::test_nitunit4 [\e[1m\e[31mX\e[m\e[m ] 1/3 test_nitunit4$TestTestSuite$test_foo \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Test-suite of module test_nitunit4::test_nitunit4 [\e[1m\e[31mX\e[m\e[m\e[1m\e[32m.\e[m\e[m ] 2/3 test_nitunit4$TestTestSuite$test_bar \e[1m\e[32m[OK]\e[m\e[m\r\e[K* Test-suite of module test_nitunit4::test_nitunit4 [\e[1m\e[31mX\e[m\e[m\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m] 3/3 test_nitunit4$TestTestSuite$test_baz \e[1m\e[31m[KO]\e[m\e[m\r\e[K* Test-suite of module test_nitunit4::test_nitunit4 [\e[1m\e[31mX\e[m\e[m\e[1m\e[32m.\e[m\e[m\e[1m\e[31mX\e[m\e[m] 3/3
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit4$TestTestSuite$test_foo
+ \e[33mtest_nitunit4/test_nitunit4.nit:22,2--26,4\e[m: Runtime Error in file .nitunit/gen_test_nitunit4.nit
+ \e[1;31mfun test_foo do\e[0m
+ ^
+ Output
+ Before Test
+ Tested method
+ After Test
+ Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31)
+
+\e[1m\e[32m[OK]\e[m\e[m test_nitunit4$TestTestSuite$test_bar
+\e[1m\e[31m[KO]\e[m\e[m test_nitunit4$TestTestSuite$test_baz
+ \e[33mtest_nitunit4/test_nitunit4.nit:32,2--34,4\e[m: Difference with expected output: diff -u test_nitunit4/test_nitunit4.sav/test_baz.res .nitunit/gen_test_nitunit4_test_baz.out1
+ \e[1;31mfun test_baz do\e[0m
+ ^
+ Output
+ 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::test_nitunit4"></testsuite><testsuite></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 in file .nitunit/gen_test_nitunit4.nit</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_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"><system-err>Before Test
+Tested method
+After Test
+</system-err></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_baz"><error>Difference with expected output: diff -u test_nitunit4/test_nitunit4.sav/test_baz.res .nitunit/gen_test_nitunit4_test_baz.out1</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
--- /dev/null
+Usage: repeating_key_xor_solve <cipher_file>
--- /dev/null
+I'm back and I'm ringin' the bell
+A rockin' on the mike while the fly girls yell
+In ecstasy in the back of me
+Well that's my DJ Deshay cuttin' all them Z's
+Hittin' hard and the girlies goin' crazy
+Vanilla's on the mike, man I'm not lazy.
+
+I'm lettin' my drug kick in
+It controls my mouth and I begin
+To just let it flow, let my concepts go
+My posse's to the side yellin', Go Vanilla Go!
+
+Smooth 'cause that's the way I will be
+And if you don't give a damn, then
+Why you starin' at me
+So get off 'cause I control the stage
+There's no dissin' allowed
+I'm in my own phase
+The girlies sa y they love me and that is ok
+And I can dance better than any kid n' play
+
+Stage 2 -- Yea the one ya' wanna listen to
+It's off my head so let the beat play through
+So I can funk it up and make it sound good
+1-2-3 Yo -- Knock on some wood
+For good luck, I like my rhymes atrocious
+Supercalafragilisticexpialidocious
+I'm an effect and that you can bet
+I can take a fly girl and make her wet.
+
+I'm like Samson -- Samson to Delilah
+There's no denyin', You can try to hang
+But you'll keep tryin' to get my style
+Over and over, practice makes perfect
+But not if you're a loafer.
+
+You'll get nowhere, no place, no time, no girls
+Soon -- Oh my God, homebody, you probably eat
+Spaghetti with a spoon! Come on and say it!
+
+VIP. Vanilla Ice yep, yep, I'm comin' hard like a rhino
+Intoxicating so you stagger like a wino
+So punks stop trying and girl stop cryin'
+Vanilla Ice is sellin' and you people are buyin'
+'Cause why the freaks are jockin' like Crazy Glue
+Movin' and groovin' trying to sing along
+All through the ghetto groovin' this here song
+Now you're amazed by the VIP posse.
+
+Steppin' so hard like a German Nazi
+Startled by the bases hittin' ground
+There's no trippin' on mine, I'm just gettin' down
+Sparkamatic, I'm hangin' tight like a fanatic
+You trapped me once and I thought that
+You might have it
+So step down and lend me your ear
+'89 in my time! You, '90 is my year.
+
+You're weakenin' fast, YO! and I can tell it
+Your body's gettin' hot, so, so I can smell it
+So don't be mad and don't be sad
+'Cause the lyrics belong to ICE, You can call me Dad
+You're pitchin' a fit, so step back and endure
+Let the witch doctor, Ice, do the dance to cure
+So come up close and don't be square
+You wanna battle me -- Anytime, anywhere
+
+You thought that I was weak, Boy, you're dead wrong
+So come on, everybody and sing this song
+
+Say -- Play that funky music Say, go white boy, go white boy go
+play that funky music Go white boy, go white boy, go
+Lay down and boogie and play that funky music till you die.
+
+Play that funky music Come on, Come on, let me hear
+Play that funky music white boy you say it, say it
+Play that funky music A little louder now
+Play that funky music, white boy Come on, Come on, Come on
+Play that funky music
+
<h1>base_simple3$B$val=</h1>
<pre><code><span class="nitcode"><span class="nc_pdef foldable" id="base_simple3$B$_val"><a id="base_simple3$B$val"></a><a id="base_simple3$B$val="></a><span class="line" id="L35"> <span class="nc_k">var</span> <span class="nc_def nc_i">val</span><span>:</span> <span class="nc_t">Int</span></span></span></span></code></pre>
<h1>base_simple3$B$init</h1>
-<pre><code><span class="nitcode"><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36"> <span class="nc_k">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
+<pre><code><span class="nitcode"><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36"> <span class="nc_k popupable" style="border-bottom: solid 2px red" title="Messages" data-content="<div><div class="dropdown"> <a data-toggle="dropdown" href="#"><b>1 message(s)</b> <span class="caret"></span></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li>Warning: init with signature in base_simple3$B</li></ul></div></div>" data-toggle="popover">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
</span><span class="line" id="L37"> <span class="nc_k">do</span>
</span><span class="line" id="L38"> <span class="nc_l">7</span><span>.</span><span class="nc_i">output</span>
</span><span class="line" id="L39"> <span class="nc_k">self</span><span>.</span><span class="nc_i">val</span> <span>=</span> <span class="nc_v nc_i">v</span>
</span></span><span class="line" id="L33">
</span><span class="nc_cdef foldable" id="base_simple3$B"><span class="line" id="L34"><span class="nc_k">class</span> <span class="nc_t">B</span>
</span><span class="nc_pdef foldable" id="base_simple3$B$_val"><a id="base_simple3$B$val"></a><a id="base_simple3$B$val="></a><span class="line" id="L35"> <span class="nc_k">var</span> <span class="nc_def nc_i">val</span><span>:</span> <span class="nc_t">Int</span>
-</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36"> <span class="nc_k">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
+</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36"> <span class="nc_k popupable" style="border-bottom: solid 2px red" title="Messages" data-content="<div><div class="dropdown"> <a data-toggle="dropdown" href="#"><b>1 message(s)</b> <span class="caret"></span></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li>Warning: init with signature in base_simple3$B</li></ul></div></div>" data-toggle="popover">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
</span><span class="line" id="L37"> <span class="nc_k">do</span>
</span><span class="line" id="L38"> <span class="nc_l">7</span><span>.</span><span class="nc_i">output</span>
</span><span class="line" id="L39"> <span class="nc_k">self</span><span>.</span><span class="nc_i">val</span> <span>=</span> <span class="nc_v nc_i">v</span>
# Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>
# JSON: not valid json
-# Errors: 'Error Parsing JSON: [1:1-1:2] Unexpected character 'n'; is acceptable instead: value'
+# Errors: 'Parsing error at line 1, position 1: Error: bad JSON entity'
# Nit: null
names::n3$::n1::P1$A::a MMethodDef names/n3.nit:31,2--32,19 a refinement (3 distinct modules)
names::n3$::n1::P1$::n0::P::p MMethodDef names/n3.nit:33,2--34,19 a refinement (3 distinct modules)
names::n0 MModule names/n0.nit:15,1--67,3 Root module
-names::Object MClass names/n0.nit:20,1--22,3
+names::Object MClass names/n0.nit:20,1--22,3 Root interface
names$Object MClassDef names/n0.nit:20,1--22,3 Root interface
names::Object::init MMethod names/n0.nit:20,1--22,3
names$Object$init MMethodDef names/n0.nit:20,1--22,3
-names::A MClass names/n0.nit:24,1--31,3
+names::A MClass names/n0.nit:24,1--31,3 A public class
names$A MClassDef names/n0.nit:24,1--31,3 A public class
-names::A::a MMethod names/n0.nit:26,2--27,13
+names::A::a MMethod names/n0.nit:26,2--27,13 A public method in a public class
names$A$a MMethodDef names/n0.nit:26,2--27,13 A public method in a public class
-names::n0::A::z MMethod names/n0.nit:29,2--30,21
+names::n0::A::z MMethod names/n0.nit:29,2--30,21 A private method in a public class
names$A$z MMethodDef names/n0.nit:29,2--30,21 A private method in a public class
-names::A0 MClass names/n0.nit:33,1--46,3
+names::A0 MClass names/n0.nit:33,1--46,3 A public subclass in the same module
names$A0 MClassDef names/n0.nit:33,1--46,3 A public subclass in the same module
names$A0$A::a MMethodDef names/n0.nit:38,2--39,19 Redefinition it the same module of a public method
names$A0$::n0::A::z MMethodDef names/n0.nit:41,2--42,19 Redefinition it the same module of a private method
names$A0$::n0::P::p MMethodDef names/n0.nit:44,2--45,19 Redefinition it the same module of a private method
-names::n0::P MClass names/n0.nit:48,1--52,3
+names::n0::P MClass names/n0.nit:48,1--52,3 A private class
names::n0$P MClassDef names/n0.nit:48,1--52,3 A private class
-names::n0::P::p MMethod names/n0.nit:50,2--51,13
+names::n0::P::p MMethod names/n0.nit:50,2--51,13 A private method in a private class
names::n0$P$p MMethodDef names/n0.nit:50,2--51,13 A private method in a private class
-names::n0::P0 MClass names/n0.nit:54,1--67,3
+names::n0::P0 MClass names/n0.nit:54,1--67,3 A private subclass introduced in the same module
names::n0$P0 MClassDef names/n0.nit:54,1--67,3 A private subclass introduced in the same module
names::n0$P0$A::a MMethodDef names/n0.nit:59,2--60,19 Redefinition it the same module of a public method
names::n0$P0$::n0::A::z MMethodDef names/n0.nit:62,2--63,19 Redefinition it the same module of a private method
names::n1$A MClassDef names/n1.nit:20,1--30,3 A refinement of a class
names::n1$A$a MMethodDef names/n1.nit:22,2--23,19 A refinement in the same class
names::n1$A$z MMethodDef names/n1.nit:25,2--26,19 A refinement in the same class
-names::n1::A::b MMethod names/n1.nit:28,2--29,13
+names::n1::A::b MMethod names/n1.nit:28,2--29,13 A public method introduced in a refinement
names::n1$A$b MMethodDef names/n1.nit:28,2--29,13 A public method introduced in a refinement
names::n1$A0 MClassDef names/n1.nit:32,1--42,3 A refinement of a subclass
names::n1$A0$A::a MMethodDef names/n1.nit:34,2--35,19 A refinement+redefinition
names::n1$A0$::n0::A::z MMethodDef names/n1.nit:37,2--38,19 A refinement+redefinition
names::n1$A0$::n0::P::p MMethodDef names/n1.nit:40,2--41,19 A refinement+redefinition
-names::A1 MClass names/n1.nit:44,1--57,3
+names::A1 MClass names/n1.nit:44,1--57,3 A subclass introduced in a submodule
names$A1 MClassDef names/n1.nit:44,1--57,3 A subclass introduced in a submodule
names$A1$A::a MMethodDef names/n1.nit:49,2--50,19 A redefinition in a subclass from a different module
names$A1$::n0::A::z MMethodDef names/n1.nit:52,2--53,19 A redefinition in a subclass from a different module
names::n1$::n0::P0$A::a MMethodDef names/n1.nit:67,2--68,19 A refinement+redefinition
names::n1$::n0::P0$::n0::A::z MMethodDef names/n1.nit:70,2--71,19 A refinement+redefinition
names::n1$::n0::P0$::n0::P::p MMethodDef names/n1.nit:73,2--74,19 A refinement+redefinition
-names::n1::P1 MClass names/n1.nit:77,1--90,3
+names::n1::P1 MClass names/n1.nit:77,1--90,3 A private subclass introduced in a different module
names::n1$P1 MClassDef names/n1.nit:77,1--90,3 A private subclass introduced in a different module
names::n1$P1$A::a MMethodDef names/n1.nit:82,2--83,19 A redefinition in a subclass from a different module
names::n1$P1$::n0::A::z MMethodDef names/n1.nit:85,2--86,19 A redefinition in a subclass from a different module
names::n1$P1$::n0::P::p MMethodDef names/n1.nit:88,2--89,19 A redefinition in a subclass from a different module
names::n2 MModule names/n2.nit:15,1--33,3 A alternative second module, used to make name conflicts
names::n2$A MClassDef names/n2.nit:20,1--27,3 A refinement of a class
-names::n2::A::b MMethod names/n2.nit:22,2--23,13
+names::n2::A::b MMethod names/n2.nit:22,2--23,13 Name conflict? A second public method
names::n2$A$b MMethodDef names/n2.nit:22,2--23,13 Name conflict? A second public method
-names::n2::A::z MMethod names/n2.nit:25,2--26,13
+names::n2::A::z MMethod names/n2.nit:25,2--26,13 Name conflict? A second private method
names::n2$A$z MMethodDef names/n2.nit:25,2--26,13 Name conflict? A second private method
-names::n2::P MClass names/n2.nit:29,1--33,3
+names::n2::P MClass names/n2.nit:29,1--33,3 Name conflict? A second private class
names::n2$P MClassDef names/n2.nit:29,1--33,3 Name conflict? A second private class
-names::n2::P::p MMethod names/n2.nit:31,2--32,13
+names::n2::P::p MMethod names/n2.nit:31,2--32,13 Name conflict? A private method in an homonym class.
names::n2$P$p MMethodDef names/n2.nit:31,2--32,13 Name conflict? A private method in an homonym class.
names1 MPackage names1.nit An alternative second module in a distinct package
names1> MGroup names1.nit An alternative second module in a distinct package
names1::names1$names::A MClassDef names1.nit:20,1--30,3 A refinement of a class
names1::names1$names::A$a MMethodDef names1.nit:22,2--23,19 A refinement in the same class
names1::names1$names::A$z MMethodDef names1.nit:25,2--26,19 A refinement in the same class
-names1::names1::A::b MMethod names1.nit:28,2--29,13
+names1::names1::A::b MMethod names1.nit:28,2--29,13 A public method introduced in a refinement
names1::names1$names::A$b MMethodDef names1.nit:28,2--29,13 A public method introduced in a refinement
names1::names1$names::A0 MClassDef names1.nit:32,1--42,3 A refinement of a subclass
names1::names1$names::A0$names::A::a MMethodDef names1.nit:34,2--35,19 A refinement+redefinition
names1::names1$names::A0$names::n0::A::z MMethodDef names1.nit:37,2--38,19 A refinement+redefinition
names1::names1$names::A0$names::n0::P::p MMethodDef names1.nit:40,2--41,19 A refinement+redefinition
-names1::A1 MClass names1.nit:44,1--57,3
+names1::A1 MClass names1.nit:44,1--57,3 A subclass introduced in a submodule
names1$A1 MClassDef names1.nit:44,1--57,3 A subclass introduced in a submodule
names1$A1$names::A::a MMethodDef names1.nit:49,2--50,19 A redefinition in a subclass from a different module
names1$A1$names::n0::A::z MMethodDef names1.nit:52,2--53,19 A redefinition in a subclass from a different module
names1::names1$names::n0::P0$names::A::a MMethodDef names1.nit:67,2--68,19 A refinement+redefinition
names1::names1$names::n0::P0$names::n0::A::z MMethodDef names1.nit:70,2--71,19 A refinement+redefinition
names1::names1$names::n0::P0$names::n0::P::p MMethodDef names1.nit:73,2--74,19 A refinement+redefinition
-names1::names1::P1 MClass names1.nit:77,1--90,3
+names1::names1::P1 MClass names1.nit:77,1--90,3 A private subclass introduced in a different module
names1::names1$P1 MClassDef names1.nit:77,1--90,3 A private subclass introduced in a different module
names1::names1$P1$names::A::a MMethodDef names1.nit:82,2--83,19 A redefinition in a subclass from a different module
names1::names1$P1$names::n0::A::z MMethodDef names1.nit:85,2--86,19 A redefinition in a subclass from a different module
--- /dev/null
+S&éstrS&éstr
--- /dev/null
+aname class sex
+Whale mammal 1
+Snake reptile 0
+++ /dev/null
-sleeping 1s
-true
-true
-sleeping 5000ns
-true
-true
-true
test.has_suffix("bt") => false
test.has_suffix("bat") => false
test.has_suffix("foot") => false
+........
+
+........
--- /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
+#alt1 intrude import core::text::ropes
+#alt2 intrude import core::text::ropes
+
+var ons = new NativeString(9)
+var base_str = "%Dégâštr"
+
+var str: String = base_str
+#alt1 str = new Concat(base_str, base_str)
+#alt2 str = new Concat(base_str, base_str.substring_from(2))
+
+var copy_len = (str.bytelen - 4).min(9)
+str.copy_to_native(ons, copy_len, 4, 0)
+print ons.to_s_with_length(copy_len)
var p = new Process("sleep", "10")
# Send some signals
-p.signal(sigalarm)
+p.signal sigalarm
p.kill
# Wait for it to die
p.wait
-var lapse = clock.lapse
# Let's be generous here, as long as it does not take 5 secs out of 10 it's OK
-print lapse.sec < 5
+print clock.lapse < 5.0
--- /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.
+
+#alt1 intrude import core::text::ropes
+import core
+
+var src_s = "S&éstr"
+var cpstr: Text = src_s
+#alt1 cpstr = new Concat(src_s, src_s)
+#alt2 cpstr = new FlatBuffer.from(src_s)
+#alt3 cpstr = cpstr.substring(1, 5)
+
+var ns = new NativeString(cpstr.bytelen)
+ns.fill_from(cpstr)
+
+print ns.to_s_with_length(cpstr.bytelen)
redef class Sys
var iface: String is lazy do
- srand
- return "localhost:{10000+20000.rand}"
+ var testid = "NIT_TESTING_ID".environ.to_i
+ return "localhost:{10000+testid}"
end
var fs_path: String = getcwd / "../lib/nitcorn/examples/www/hello_world/dir" is lazy
# assert undefined_identifier
fun foo do end
+ # a 'failure' unit test (does not parse)
+ # assert !@#$%^&*()
fun foo1(a, b: Int) do end
private fun foo2: Bool do return true
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
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+#
+# 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_postgres_native
+
+import postgresql::native_postgres
+
+var db = new NativePostgres.connectdb("dbname=postgres")
+assert postgres_open: db.status.is_ok else print_error db.error
+
+var result = db.exec("CREATE TABLE IF NOT EXISTS animals (aname TEXT PRIMARY KEY, class TEXT NOT NULL, sex INTEGER)")
+assert postgres_create_table: result.status.is_ok else print_error db.error
+
+result = db.exec("INSERT INTO animals VALUES('Whale', 'mammal', 1)")
+assert postgres_insert_1: result.status.is_ok else print_error db.error
+
+result = db.exec("INSERT INTO animals VALUES('Snake', 'reptile', 0)")
+assert postgres_insert_2: result.status.is_ok else print_error db.error
+
+result = db.exec("SELECT * FROM animals")
+assert postgres_select: result.status.is_ok else print_error db.error
+
+assert postgres_ntuples: result.ntuples == 2 else print_error db.error
+assert postgres_nfields: result.nfields == 3 else print_error db.error
+assert postgres_fname: result.fname(0) == "aname" else print_error db.error
+assert postgres_isnull: result.is_null(0,0) == false else print_error db.error
+assert postgres_value: result.value(0,0) == "Whale" else print_error db.error
+
+var cols: Int = result.nfields
+var rows: Int = result.ntuples
+var fields: String = ""
+for c in [0..cols[ do fields += result.fname(c) + " "
+print fields
+for i in [0..rows[ do
+ fields = ""
+ for j in [0..cols[ do fields += result.value(i, j) + " "
+ print fields
+end
+
+result = db.exec("DELETE FROM animals WHERE aname = 'Lioness'")
+assert postgres_delete_1: result.status.is_ok else print_error db.error
+
+result = db.exec("DELETE FROM animals WHERE aname = 'Snake'")
+assert postgres_delete_2: result.status.is_ok else print_error db.error
+
+result = db.prepare("PREPARED_INSERT", "INSERT INTO animals(aname, class, sex) VALUES ($1, $2, $3)", 3)
+assert postgres_prepare: result.status.is_ok else print_error db.error
+
+result = db.exec("DELETE FROM animals WHERE aname = 'Frog'")
+assert postgres_delete_3: result.status.is_ok else print_error db.error
+
+var values = ["Frog", "Anphibian", "1"]
+var lengths = [values[0].length, values[1].length, values[2].length]
+var formats = [0,0,0]
+result = db.exec_prepared("PREPARED_INSERT", 3, values, lengths, formats,0)
+assert postgres_exec_prepared: result.status.is_ok else print_error db.error
+
+result = db.exec("DROP TABLE animals")
+assert postgres_drop_table: result.status.is_ok else print_error db.error
+db.finish
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+#
+# 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_postgres_nity
+
+import postgresql::postgres
+
+var db = new Postgres.open("dbname=postgres")
+assert open_db: not db.is_closed else print db.error
+
+assert create_table: db.create_table("IF NOT EXISTS users (uname TEXT PRIMARY KEY, pass TEXT NOT NULL, activated INTEGER, perc FLOAT)") else
+ print db.error
+end
+
+assert insert1: db.insert("INTO users VALUES('Bob', 'zzz', 1, 77.7)") else
+ print db.error
+end
+
+assert insert2: db.insert("INTO users VALUES('Guilherme', 'xxx', 1, 88)") else
+ print db.error
+end
+
+var result = db.raw_execute("SELECT * FROM users")
+
+assert raw_exec: result.is_ok else print db.error
+
+assert postgres_nfields: result.nfields == 4 else print_error db.error
+assert postgres_fname: result.fname(0) == "uname" else print_error db.error
+assert postgres_isnull: result.is_null(0,0) == false else print_error db.error
+assert postgres_value: result.value(0,0) == "Bob" else print_error db.error
+
+assert drop_table: db.execute("DROP TABLE users") else print db.error
+
+db.finish
+
+assert db.is_closed else print db.error
var db = new NativeSqlite3.open(filename.to_cstring)
assert sqlite_open: db.error.is_ok
-db.exec(create_req)
+db.exec(create_req.to_cstring)
assert sqlite_create_table: db.error.is_ok
-db.exec(insert_req_1)
+db.exec(insert_req_1.to_cstring)
assert sqlite_insert_1: db.error.is_ok
-db.exec(insert_req_2)
+db.exec(insert_req_2.to_cstring)
assert sqlite_insert_2: db.error.is_ok
-var stmt = db.prepare(select_req)
+var stmt = db.prepare(select_req.to_cstring)
assert sqlite_select: db.error.is_ok
-if stmt == null then
+if stmt.address_is_null then
print "Prepared failed got: {db.error.to_s}"
abort
end
db = new NativeSqlite3.open(filename.to_cstring)
assert sqlite_reopen: db.error.is_ok
-stmt = db.prepare(select_req)
+stmt = db.prepare(select_req.to_cstring)
assert sqlite_reselect: db.error.is_ok
-assert stmt != null
+assert not stmt.address_is_null
stmt.step
assert sqlite_column_0_0_reopened: stmt.column_text(0).to_s == "Bob"
print("test.has_suffix(\"bat\") => {test.has_suffix("bat")}")
print("test.has_suffix(\"foot\") => {test.has_suffix("foot")}")
+print "........"
+print "/".substring(7, 1)
+print "........"
export LANG=C
export LC_ALL=C
export NIT_TESTING=true
+# Use the pid as a collision prevention
+export NIT_TESTING_ID=$$
export NIT_SRAND=0
unset NIT_DIR