From: Jean Privat Date: Tue, 24 May 2016 23:12:34 +0000 (-0400) Subject: Merge: Reworked crypto.nit to introduce basic XOR attacks X-Git-Url: http://nitlanguage.org?hp=c876a2c009bcf3b06b8348d98f7ea85cd66fb600 Merge: Reworked crypto.nit to introduce basic XOR attacks lib/crypto: `crypto.nit` was getting pretty overwhelmed with the upcoming changes so it was exploded into a package. Introduced classes for cipher management to help with attacks. lib/crapto: Introduced 2 attacks on basic XOR ciphers. Pull-Request: #2080 Reviewed-by: Jean Privat Reviewed-by: Lucas Bajolet --- diff --git a/.gitattributes b/.gitattributes index ca8864b..02c0677 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,4 +6,5 @@ tables_nit.c -diff c_src/** -diff tests/sav/**/*.res -whitespace +lib/popcorn/tests/res/*.res -whitespace *.patch -whitespace diff --git a/.gitignore b/.gitignore index 80ef46d..a739d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.bak *.swp +*.swo *~ .project EIFGENs diff --git a/benchmarks/csv/README.md b/benchmarks/csv/README.md new file mode 100644 index 0000000..5f913bd --- /dev/null +++ b/benchmarks/csv/README.md @@ -0,0 +1,11 @@ +# 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 diff --git a/benchmarks/csv/csv_bench.sh b/benchmarks/csv/csv_bench.sh new file mode 100755 index 0000000..badd1e6 --- /dev/null +++ b/benchmarks/csv/csv_bench.sh @@ -0,0 +1,103 @@ +#!/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 diff --git a/benchmarks/csv/scripts/JavaCSV.java b/benchmarks/csv/scripts/JavaCSV.java new file mode 100644 index 0000000..f8264ca --- /dev/null +++ b/benchmarks/csv/scripts/JavaCSV.java @@ -0,0 +1,18 @@ +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 r = parser.getRecords(); + } catch(Exception e) { + System.err.println("Major fail"); + } + } +} diff --git a/benchmarks/csv/scripts/csv_gen.nit b/benchmarks/csv/scripts/csv_gen.nit new file mode 100644 index 0000000..123dbb6 --- /dev/null +++ b/benchmarks/csv/scripts/csv_gen.nit @@ -0,0 +1,61 @@ +# 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) diff --git a/benchmarks/csv/scripts/go_csv.go b/benchmarks/csv/scripts/go_csv.go new file mode 100644 index 0000000..5fff932 --- /dev/null +++ b/benchmarks/csv/scripts/go_csv.go @@ -0,0 +1,18 @@ +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) } +} diff --git a/benchmarks/csv/scripts/nit_csv.nit b/benchmarks/csv/scripts/nit_csv.nit new file mode 100644 index 0000000..c8422d1 --- /dev/null +++ b/benchmarks/csv/scripts/nit_csv.nit @@ -0,0 +1,25 @@ +# 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 diff --git a/benchmarks/csv/scripts/python_csv.py b/benchmarks/csv/scripts/python_csv.py new file mode 100644 index 0000000..d8addda --- /dev/null +++ b/benchmarks/csv/scripts/python_csv.py @@ -0,0 +1,4 @@ +import sys +from pandas import read_csv + +csv = read_csv(sys.argv[1]) diff --git a/benchmarks/csv/scripts/python_stdcsv.py b/benchmarks/csv/scripts/python_stdcsv.py new file mode 100644 index 0000000..b78cb15 --- /dev/null +++ b/benchmarks/csv/scripts/python_stdcsv.py @@ -0,0 +1,8 @@ +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) diff --git a/benchmarks/csv/scripts/ruby_csv.rb b/benchmarks/csv/scripts/ruby_csv.rb new file mode 100644 index 0000000..6b1fe02 --- /dev/null +++ b/benchmarks/csv/scripts/ruby_csv.rb @@ -0,0 +1,3 @@ +require 'csv' + +CSV.read(ARGV.first) diff --git a/contrib/asteronits/Makefile b/contrib/asteronits/Makefile index f110c83..a5e910a 100644 --- a/contrib/asteronits/Makefile +++ b/contrib/asteronits/Makefile @@ -6,17 +6,17 @@ all: bin/asteronits bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit linux) ${NITC} pre-build ${NITC} src/asteronits.nit -m linux -o $@ -bin/texture_atlas_parser: src/texture_atlas_parser.nit - ${NITC} src/texture_atlas_parser.nit -o $@ +bin/texture_atlas_parser: ../../lib/gamnit/texture_atlas_parser.nit + ${NITC} ../../lib/gamnit/texture_atlas_parser.nit -o $@ src/controls.nit: art/controls.svg make -C ../inkscape_tools/ ../inkscape_tools/bin/svg_to_png_and_nit art/controls.svg -a assets/ -s src/ -x 2.0 -g -src/spritesheet_city.nit: bin/texture_atlas_parser +src/spritesheet.nit: bin/texture_atlas_parser bin/texture_atlas_parser art/sheet.xml --dir src/ -n spritesheet -pre-build: src/controls.nit src/spritesheet_city.nit +pre-build: src/controls.nit src/spritesheet.nit check: bin/asteronits NIT_TESTING=true bin/asteronits diff --git a/contrib/asteronits/src/android.nit b/contrib/asteronits/src/android.nit index 4042f6d..e7ac9df 100644 --- a/contrib/asteronits/src/android.nit +++ b/contrib/asteronits/src/android.nit @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import ::android::platform +import ::android import ::android::vibration import asteronits diff --git a/contrib/asteronits/src/touch_ui.nit b/contrib/asteronits/src/touch_ui.nit index 3971426..388cd35 100644 --- a/contrib/asteronits/src/touch_ui.nit +++ b/contrib/asteronits/src/touch_ui.nit @@ -62,16 +62,16 @@ redef class App 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 @@ -98,20 +98,20 @@ redef class App # 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 diff --git a/contrib/benitlux/.gitignore b/contrib/benitlux/.gitignore index 562fb6d..ccbe098 100644 --- a/contrib/benitlux/.gitignore +++ b/contrib/benitlux/.gitignore @@ -1,4 +1,4 @@ -src/benitlux_restful.nit +src/server/benitlux_restful.nit *.db *.email benitlux_corrections.txt diff --git a/contrib/benitlux/Makefile b/contrib/benitlux/Makefile index c14d648..2142041 100644 --- a/contrib/benitlux/Makefile +++ b/contrib/benitlux/Makefile @@ -1,19 +1,19 @@ SERVER ?= localhost:8080 -all: server +all: server bin/report bin/benitlux server: bin/benitlux_daily bin/benitlux_web -bin/benitlux_daily: $(shell ../../bin/nitls -M src/benitlux_daily.nit) +bin/benitlux_daily: $(shell ../../bin/nitls -M src/server/benitlux_daily.nit) mkdir -p bin/ - ../../bin/nitc -o $@ src/benitlux_daily.nit + ../../bin/nitc -o $@ src/server/benitlux_daily.nit -bin/benitlux_web: $(shell ../../bin/nitls -M src/benitlux_web.nit) src/benitlux_restful.nit +bin/benitlux_web: $(shell ../../bin/nitls -M src/server/server.nit) src/server/benitlux_restful.nit mkdir -p bin/ - ../../bin/nitc -o $@ src/benitlux_web.nit -D iface=$(SERVER) + ../../bin/nitc -o $@ src/server/server.nit -D iface=$(SERVER) -pre-build: src/benitlux_restful.nit -src/benitlux_restful.nit: $(shell ../../bin/nitls -M src/benitlux_controller.nit) - ../../bin/nitrestful -o $@ src/benitlux_controller.nit +pre-build: src/server/benitlux_restful.nit +src/server/benitlux_restful.nit: $(shell ../../bin/nitls -M src/server/benitlux_controller.nit) + ../../bin/nitrestful -o $@ src/server/benitlux_controller.nit # --- # Report @@ -23,3 +23,64 @@ bin/report: $(shell ../../bin/nitls -M src/report.nit) report: bin/report bin/report + +# --- +# GTK+ client + +bin/benitlux: $(shell ../../bin/nitls -M src/client/client.nit) + mkdir -p bin/ + ../../bin/nitc -o bin/benitlux src/client/client.nit -m linux -D benitlux_rest_server_uri=http://$(SERVER)/ + +# --- +# Android + +# Main icon +android/res/drawable-hdpi/icon.png: + ../inkscape_tools/bin/svg_to_icons art/icon.svg --android --out android/res/ + +# Notification icon, white only +android/res/drawable-hdpi/notif.png: + ../inkscape_tools/bin/svg_to_icons art/notif.svg --android --out android/res/ --name notif + +android-res: android/res/drawable-hdpi/icon.png android/res/drawable-hdpi/notif.png + +# Dev / debug app +android: bin/benitlux.apk +bin/benitlux.apk: $(shell ../../bin/nitls -M src/client/android.nit) android-res + mkdir -p bin/ res/ + ../../bin/nitc -o $@ src/client/android.nit -m src/client/features/debug.nit \ + -D benitlux_rest_server_uri=http://$(SERVER)/ + +# Pure portable prototype, for comparison +bin/proto.apk: $(shell ../../bin/nitls -M src/client/android_proto.nit) android-res + mkdir -p bin/ res/ + ../../bin/nitc -o $@ src/client/android_proto.nit \ + -D benitlux_rest_server_uri=http://$(SERVER)/ + +# Release version +android-release: $(shell ../../bin/nitls -M src/client/android.nit) android-res + mkdir -p bin/ res/ + ../../bin/nitc -o bin/benitlux.apk src/client/android.nit \ + -D benitlux_rest_server_uri=http://xymus.net/benitlux/ --release + +# --- +# iOS + +ios: bin/benitlux.app +bin/benitlux.app: $(shell ../../bin/nitls -M src/client/ios.nit) ios/AppIcon.appiconset/Contents.json + mkdir -p bin/ + rm -rf bin/benitlux.app/ + ../../bin/nitc -o bin/benitlux.app src/client/ios.nit -D benitlux_rest_server_uri=http://$(SERVER)/ + +bin/proto.app: $(shell ../../bin/nitls -M src/client/ios_proto.nit) ios/AppIcon.appiconset/Contents.json + mkdir -p bin/ res/ + ../../bin/nitc -o $@ src/client/ios_proto.nit \ + -D benitlux_rest_server_uri=http://$(SERVER)/ + +ios-release: $(shell ../../bin/nitls -M src/client/ios.nit) ios/AppIcon.appiconset/Contents.json + mkdir -p bin/ + ../../bin/nitc -o bin/benitlux.app src/client/ios.nit -D benitlux_rest_server_uri=http://$(SERVER)/ + +ios/AppIcon.appiconset/Contents.json: art/icon.svg + mkdir -p ios + ../inkscape_tools/bin/svg_to_icons art/icon.svg --ios --out ios/AppIcon.appiconset/ diff --git a/contrib/benitlux/README.md b/contrib/benitlux/README.md index 6f4ce46..b8aed58 100644 --- a/contrib/benitlux/README.md +++ b/contrib/benitlux/README.md @@ -1,21 +1,45 @@ -An unofficial mailing list and other tools to keep faithful bargoers informed of the beers available at the excellent Brasserie Bénélux. +An unofficial app and mailing list to keep faithful bargoers informed of the beers available at the excellent Brasserie Bénélux. -This project is composed of two softwares: +This project is composed of three softwares: -* a Web interface to subscribe and unsubscribe, -* and a daily background program which updates the BD and send emails. +* A mobile app and social network, +* a server with a RESTful API for the mobile app and a web interface to subscribe to the mailing list +* and a daily background program which updates the DB and send emails. -The web interface is currently published at +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 @@ -25,10 +49,9 @@ The Web interface will be accessible at - [x] Daily mailer - [x] Web interface - [x] Serialization and deserialization of data classes -- [ ] Android app -- [ ] iOS app +- [x] Android app +- [x] iOS app - [ ] Charlevoix location support -- [ ] Customize mails (daily, on change, per locations) - [ ] Authenticate unsubscribe actions over GET -- [ ] Social network and location updates +- [x] Social network and location updates - [ ] Event updates diff --git a/contrib/benitlux/android/res/.gitignore b/contrib/benitlux/android/res/.gitignore new file mode 100644 index 0000000..46ce728 --- /dev/null +++ b/contrib/benitlux/android/res/.gitignore @@ -0,0 +1 @@ +drawable* diff --git a/contrib/benitlux/android/res/values/styles.xml b/contrib/benitlux/android/res/values/styles.xml new file mode 100644 index 0000000..c435c5f --- /dev/null +++ b/contrib/benitlux/android/res/values/styles.xml @@ -0,0 +1,4 @@ + + + #000000 + diff --git a/contrib/benitlux/art/icon.svg b/contrib/benitlux/art/icon.svg new file mode 100644 index 0000000..779a3e3 --- /dev/null +++ b/contrib/benitlux/art/icon.svg @@ -0,0 +1,88 @@ + + + + + + + + + + image/svg+xml + + + + + + + + B + + + diff --git a/contrib/benitlux/art/notif.svg b/contrib/benitlux/art/notif.svg new file mode 100644 index 0000000..871b01c --- /dev/null +++ b/contrib/benitlux/art/notif.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + B + + + diff --git a/contrib/benitlux/ios/.gitignore b/contrib/benitlux/ios/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/contrib/benitlux/ios/.gitignore @@ -0,0 +1 @@ +* diff --git a/contrib/benitlux/net.xymus.benitlux.txt b/contrib/benitlux/net.xymus.benitlux.txt new file mode 100644 index 0000000..fa717a9 --- /dev/null +++ b/contrib/benitlux/net.xymus.benitlux.txt @@ -0,0 +1,11 @@ +Categories:Nit,Internet +License:Apache2 +Web Site:http://xymus.net/benitlux +Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/contrib/benitlux +Issue Tracker:https://github.com/nitlang/nit/issues + +Summary:Mobile client for the Benitlux social network +Description: +View the beer menu, rate beers, view community rating, and receive notifications +of the daily menu changes and when friends are on location. +. diff --git a/contrib/benitlux/package.ini b/contrib/benitlux/package.ini index e53853c..38eeee8 100644 --- a/contrib/benitlux/package.ini +++ b/contrib/benitlux/package.ini @@ -1,12 +1,13 @@ [package] name=benitlux -tags=network +tags=mobile,web maintainer=Alexis Laferrière license=Apache-2.0 [upstream] browse=https://github.com/nitlang/nit/tree/master/contrib/benitlux/ git=https://github.com/nitlang/nit.git git.directory=contrib/benitlux/ -homepage=http://nitlanguage.org +homepage=http://xymus.net/benitlux/ issues=https://github.com/nitlang/nit/issues -tryit=http://benitlux.xymus.net/ +tryit=http://xymus.net/benitlux/ +apk=http://nitlanguage.org/fdroid/apk/tnitter.apk diff --git a/contrib/benitlux/src/benitlux_model.nit b/contrib/benitlux/src/benitlux_model.nit index 987047f..48a807a 100644 --- a/contrib/benitlux/src/benitlux_model.nit +++ b/contrib/benitlux/src/benitlux_model.nit @@ -234,6 +234,14 @@ class CheckinReport 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 diff --git a/contrib/benitlux/src/client/android.nit b/contrib/benitlux/src/client/android.nit new file mode 100644 index 0000000..f3391bf --- /dev/null +++ b/contrib/benitlux/src/client/android.nit @@ -0,0 +1,268 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Android variant improved with platform specific services +module android is + android_manifest_activity """android:theme="@android:style/Theme.DeviceDefault" """ + android_api_min 16 # For BigTextStyle + android_api_target 16 +end + +import ::android::portrait +import ::android::toast +import ::android::wifi +import ::android::service::at_boot + +import client +import push +import checkins + +redef class App + + redef fun on_create + do + super + + # Launch service with app, if it wasn't already launched at boot + start_service + end + + # Use Android toasts if there is an activity, otherwise fallback on the log + redef fun feedback(text) + do + if activities.not_empty then + app.toast(text.to_s, false) + else super + end + + # Register to callback `async_wifi_scan_available` when a wifi scan is available + private fun notify_on_wifi_scan(context: NativeContext) + import async_wifi_scan_available in "Java" `{ + + android.content.IntentFilter filter = new android.content.IntentFilter(); + filter.addAction(android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + final int final_self = self; + + context.registerReceiver( + new android.content.BroadcastReceiver() { + @Override + public void onReceive(android.content.Context context, android.content.Intent intent) { + if (intent.getAction().equals(android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + App_async_wifi_scan_available(final_self); + } + } + }, filter); + `} + + private fun async_wifi_scan_available do run_on_ui_thread task_on_wifi_scan_available + + private var task_on_wifi_scan_available = new WifiScanAvailable is lazy +end + +redef class Service + redef fun on_start_command(intent, flags, id) + do + app.notify_on_wifi_scan native + + # Check token validity + (new PushHttpRequest("push/check_token?token={app.token}")).start + + return start_sticky + end +end + +# Task ran on the UI thread when a wifi scan is available +private class WifiScanAvailable + super Task + + redef fun main + do + jni_env.push_local_frame 4 + var manager = app.native_context.wifi_manager + var networks = manager.get_scan_results + var found_ben = false + for i in networks.length.times do + jni_env.push_local_frame 4 + var net = networks[i] + var ssid = net.ssid.to_s + + # TODO use BSSID instead + #var bssid = net.bssid.to_s + var target_ssids = ["Benelux"] + if target_ssids.has(ssid) then # and bssid == "C8:F7:33:81:B0:E6" then + found_ben = true + break + end + jni_env.pop_local_frame + end + jni_env.pop_local_frame + + if found_ben then + app.on_check_in + else app.on_check_out + end +end + +redef class SectionTitle + init do set_text_style(native, app.native_context) + + private fun set_text_style(view: NativeTextView, context: NativeContext) in "Java" `{ + view.setTextAppearance(context, android.R.style.TextAppearance_Large); + `} +end + +redef class ItemView + init do set_backgroud(native, app.native_context) + + private fun set_backgroud(view: NativeView, context: NativeContext) in "Java" `{ + int color = context.getResources().getIdentifier("item_background", "color", context.getPackageName()); + view.setBackgroundResource(color); + `} +end + +# Use Android notifications +redef fun notify(title, content, id) +do + var service = app.service + assert service != null + native_notify(service.native, id, title.to_java_string, content.to_java_string) +end + +private fun native_notify(context: NativeService, id: Int, title, content: JavaString) +in "Java" `{ + int icon = context.getResources().getIdentifier( + "notif", "drawable", context.getPackageName()); + + android.app.Notification.BigTextStyle style = + new android.app.Notification.BigTextStyle(); + style.bigText(content); + + android.content.Intent intent = new android.content.Intent( + context, nit.app.NitActivity.class); + android.app.PendingIntent pendingIntent = android.app.PendingIntent.getActivity( + context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT); + + android.app.Notification notif = new android.app.Notification.Builder(context) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(icon) + .setAutoCancel(true) + .setOngoing(false) + .setStyle(style) + .setContentIntent(pendingIntent) + .setDefaults(android.app.Notification.DEFAULT_SOUND | + android.app.Notification.DEFAULT_LIGHTS) + .build(); + + android.app.NotificationManager notificationManager = + (android.app.NotificationManager)context.getSystemService(android.content.Context.NOTIFICATION_SERVICE); + + notificationManager.notify((int)id, notif); +`} + + +# Use `RatingBar` as the beer rating control +redef class BeerView + redef fun setup_stars(rating) + do + var title = "Review %0".t.format(beer_info.beer.name).to_java_string + native_setup_stars(app.native_context, top_line_layout.native, rating, title, app.user != null) + end + + private fun native_setup_stars(context: NativeContext, layout: NativeViewGroup, rating: Int, title: JavaString, loggedin: Bool) + import on_review in "Java" `{ + // Set an indicator/non-interactive display + final android.widget.RatingBar view = new android.widget.RatingBar( + context, null, android.R.attr.ratingBarStyleIndicator); + view.setNumStars(5); + view.setRating(rating); + view.setIsIndicator(true); + + final android.view.ViewGroup.MarginLayoutParams params = new android.view.ViewGroup.MarginLayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.FILL_PARENT); + layout.addView(view, params); + + // Make some variables final to used in anonymous class and delayed methods + final android.content.Context final_context = context; + final long final_rating = rating; + final String final_title = title; + final boolean final_loggedin = loggedin; + + final int final_self = self; + BeerView_incr_ref(self); // Nit GC + + view.setOnTouchListener(new android.view.View.OnTouchListener() { + @Override + public boolean onTouch(android.view.View v, android.view.MotionEvent event) { + if (event.getAction() != android.view.MotionEvent.ACTION_UP) return true; + + // Don't show dialog if not logged in + if (!final_loggedin) { + android.widget.Toast toast = android.widget.Toast.makeText( + final_context, "You must login first to post reviews", + android.widget.Toast.LENGTH_SHORT); + toast.show(); + return true; + } + + // Build dialog with a simple interactive RatingBar + final android.app.AlertDialog.Builder dialog_builder = new android.app.AlertDialog.Builder(final_context); + final android.widget.RatingBar rating = new android.widget.RatingBar(final_context); + rating.setNumStars(5); + rating.setStepSize(1.0f); + rating.setRating(final_rating); + + // Header bar + int icon = final_context.getResources().getIdentifier("notif", "drawable", final_context.getPackageName()); + dialog_builder.setIcon(icon); + dialog_builder.setTitle(final_title); + + // Rating control + android.widget.LinearLayout l = new android.widget.LinearLayout(final_context); + l.addView(rating, params); + l.setHorizontalGravity(android.view.Gravity.CENTER_HORIZONTAL); + dialog_builder.setView(l); + + // OK button + dialog_builder.setPositiveButton(android.R.string.ok, + new android.content.DialogInterface.OnClickListener() { + public void onClick(android.content.DialogInterface dialog, int which) { + dialog.dismiss(); + + long r = (long)rating.getRating(); + view.setRating(r); // Update static control + view.invalidate(); // For not refreshing bug + + BeerView_on_review(final_self, r); // Callback + BeerView_decr_ref(final_self); // Nit GC + } + }); + + // Cancel button + dialog_builder.setNegativeButton(android.R.string.cancel, + new android.content.DialogInterface.OnClickListener() { + public void onClick(android.content.DialogInterface dialog, int id) { + dialog.cancel(); + BeerView_decr_ref(final_self); // Nit GC + } + }); + + dialog_builder.create(); + dialog_builder.show(); + return true; + } + }); + `} +end diff --git a/contrib/benitlux/src/client/android_proto.nit b/contrib/benitlux/src/client/android_proto.nit new file mode 100644 index 0000000..22f0953 --- /dev/null +++ b/contrib/benitlux/src/client/android_proto.nit @@ -0,0 +1,28 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Android variant without modification, pure prototype +# +# Usually, compiling with `nitc -m android client.nit` is enough. +# In this case, for research purposes we set a different `app_namespace`. +# This allows both the proto and the adaptation to be installed on the same device. +module android_proto is + app_name "Ben Proto" + app_namespace "net.xymus.benitlux_proto" + android_api_target 16 +end + +import ::android + +import client diff --git a/contrib/benitlux/src/client/base.nit b/contrib/benitlux/src/client/base.nit new file mode 100644 index 0000000..bcc1efd --- /dev/null +++ b/contrib/benitlux/src/client/base.nit @@ -0,0 +1,155 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Common services for the Benitlux app +module base + +import app::ui +import app::data_store +import app::http_request +import android::aware +import json::serialization + +import benitlux_model +import translations + +# Show debug output? +fun debug: Bool do return true + +# Root URI of the remote RESTfule server +fun benitlux_rest_server_uri: String do return "http://localhost:8080/" + +redef class App + + # Current connection token, or "none" + var token: String is lazy, writable do + var token = app.data_store["token"] + if token isa String then return token + return "none" + end + + # Name of the currently logged in user + var user: nullable String is lazy, writable do + var user = app.data_store["user"] + if user isa nullable String then return user + return null + end + + # Event when user logs in or out + fun on_log_in do on_save_state + + redef fun on_save_state + do + app.data_store["user"] = user + app.data_store["token"] = token + super + end + + # Has this app state been restored yet? + var restored = false + + redef fun on_restore_state + do + super + + # TODO this may happen before the lazy loading above + restored = true + + if token != "none" then on_log_in + end + + # Show simple feedback to the user on important errors + fun feedback(text: Text) do print_error text +end + +# Show a notification to the user +fun notify(title, content: Text, uniqueness_id: Int) +do print "Notification {uniqueness_id}: {title}; {content}" + +# View for an item in a list, like a beer or a person +abstract class ItemView + super View +end + +# Basic async HTTP request for this app +# +# Note that connection errors are passed to `on_fail`, and +# server errors or authentification errors are received by `on_load` +# and should be passed to `intercept_error`. +class BenitluxHttpRequest + super AsyncHttpRequest + + redef fun rest_server_uri do return benitlux_rest_server_uri + + redef var rest_action + + redef fun on_fail(error) + do + if error isa IOError then + # This should be a normal network error like being offline. + # Print to log, but don't show to the user. + print_error error.class_name + else + # This could be a deserialization error, + # it may be related to an outdated client. + # Report to user. + print_error "Request Error: {rest_server_uri / rest_action} with {error}" + app.feedback "Request Error: {error}" + end + end + + # Intercept known server side errors + fun intercept_error(res: nullable Object): Bool + do + if res isa BenitluxTokenError then + app.token = "none" + app.user = null + return true + else if res isa BenitluxError then + app.feedback((res.user_message or else res.message).t) + return true + else if res isa Error then + app.feedback res.message.t + return true + end + return false + end +end + +# Async request with services to act on the windows of the app +class WindowHttpRequest + super BenitluxHttpRequest + + autoinit window, rest_action + + # Type of the related `window` + type W: Window + + # `Window` on which to apply the results of this request + var window: W + + # `Views` to disable while this request is in progress + var affected_views = new Array[View] + + redef fun before do for view in affected_views do view.enabled = false + + redef fun after do for view in affected_views do view.enabled = true +end + +redef class Text + # Ellipsize `self` so it fits within `max_length` characters + # + # FIXME Remove this when labels are correctly ellipsized on iOS. + fun ellipsize: Text do return self +end diff --git a/contrib/benitlux/src/client/client.nit b/contrib/benitlux/src/client/client.nit new file mode 100644 index 0000000..e2fc2a9 --- /dev/null +++ b/contrib/benitlux/src/client/client.nit @@ -0,0 +1,44 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Portable Benitlux app +module client is + app_name "Benitlux" + app_version(0, 3, git_revision) + app_namespace "net.xymus.benitlux" +end + +import home_views +import beer_views +import social_views +import user_views + +# --- +# Services + +redef class Deserializer + redef fun deserialize_class(name) + do + if name == "Array[Beer]" then return new Array[Beer].from_deserializer(self) + if name == "Array[User]" then return new Array[User].from_deserializer(self) + if name == "Array[BeerBadge]" then return new Array[BeerBadge].from_deserializer(self) + if name == "Array[BeerAndRatings]" then return new Array[BeerAndRatings].from_deserializer(self) + if name == "Array[String]" then return new Array[String].from_deserializer(self) + if name == "Array[UserAndFollowing]" then return new Array[UserAndFollowing].from_deserializer(self) + return super + end +end + +set_fr +super diff --git a/contrib/benitlux/src/client/features/checkins.nit b/contrib/benitlux/src/client/features/checkins.nit new file mode 100644 index 0000000..3be087e --- /dev/null +++ b/contrib/benitlux/src/client/features/checkins.nit @@ -0,0 +1,179 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# On location checkin services +module checkins + +import client + +redef class App + + # Should we share our checkins with the server and friends? + fun share_checkins: Bool + do return app.data_store["share_checkins"].as(nullable Bool) or else true + + # Should we share our checkins with the server and friends? + fun share_checkins=(value: Bool) + do + # Notify server + if currently_on_location then + if value then + server_check_in + else server_check_out + end + + app.data_store["share_checkins"] = value + end + + # Are we currently at the location? + fun currently_on_location: Bool + do return app.data_store["currently_on_location"].as(nullable Bool) or else false + + # Are we currently at the location? + fun currently_on_location=(value: Bool) do app.data_store["currently_on_location"] = value + + # Request beer menu from the server + # + # It includes a diff if `checkins` remembers a previous visit. + fun request_menu + do + var checkins = checkins + var since = checkins.latest + if since != null then + var today = today + if since == today then + since = checkins.previous + end + end + + (new MenuHttpRequest("rest/since?token={token}&date={since or else ""}")).start + end + + # User checks in + fun on_check_in + do + if currently_on_location then return + + if share_checkins then server_check_in + + currently_on_location = true + request_menu + checkins.update today + end + + # User checks out + fun on_check_out + do + if not currently_on_location then return + + if share_checkins then server_check_out + currently_on_location = false + end + + # Notify server of checkin + private fun server_check_in do (new BenitluxHttpRequest("rest/checkin?token={app.token}&is_in=true")).start + + # Notify server of checkout + private fun server_check_out do (new BenitluxHttpRequest("rest/checkin?token={app.token}&is_in=false")).start + + # History of the last 1 or 2 checkins + var checkins = new SimpleMemory + + redef fun on_save_state + do + super + app.data_store["checkins"] = checkins + end + + redef fun on_restore_state + do + var checkins = app.data_store["checkins"] + if checkins isa SimpleMemory then self.checkins = checkins + + super + end +end + +# Request the menu from the server for a notification +class MenuHttpRequest + super BenitluxHttpRequest + + redef fun on_load(data) + do + if not data isa Array[BeerAndRatings] then + on_fail new Error("Server sent unexpected data {data or else "null"}") + return + end + + var content = data.beers_to_notification + + notify("Passing by the Benelux?".t, content, 2) + end +end + +# --- +# Support services + +# Memory of an element and the previous one, avoiding duplication +# +# Used to remember the last day at the location, +# ignoring multiple reports on the same day. +class SimpleMemory + serialize + + # Before latest remembered entry + var previous: nullable String = null + + # Last remembered entry + var latest: nullable String = null + + # Update `latest` if `value` is different + fun update(value: String) + do + if value == latest then return + + previous = latest + latest = value + end +end + +# --- +# UI + +redef class UserWindow + + private var lbl_checkins_options_title = new Label(parent=layout, + text="Share options".t) + + private var chk_share_checkins = new CheckBox(parent=layout, + text="Share checkins with your friends".t) + + init + do + chk_share_checkins.is_checked = app.share_checkins + lbl_checkins_options_title.size = 1.5 + end + + redef fun on_event(event) + do + super + + if event isa ToggleEvent then + var sender = event.sender + if sender == chk_share_checkins then + app.share_checkins = sender.is_checked + end + end + end +end diff --git a/contrib/benitlux/src/client/features/debug.nit b/contrib/benitlux/src/client/features/debug.nit new file mode 100644 index 0000000..ae7be3c --- /dev/null +++ b/contrib/benitlux/src/client/features/debug.nit @@ -0,0 +1,67 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Debugging features accessible from the user preference menu +module debug + +import client +import push +import checkins + +redef class UserWindow + + private var layout_debug = new VerticalLayout(parent=layout) + + private var lbl_debug_title = new Label(parent=layout_debug, + text="Debug options".t) + + private var but_test_notif = new Button(parent=layout_debug, + text="Test notifications".t) + + private var but_test_checkin = new Button(parent=layout_debug, + text="Test checkin".t) + + private var but_test_checkout = new Button(parent=layout_debug, + text="Test checkout".t) + + private var but_test_menu = new Button(parent=layout_debug, + text="Test menu diff".t) + + init + do + lbl_debug_title.size = 1.5 + + for c in [but_test_notif, but_test_checkin, but_test_checkout, but_test_menu] do + c.observers.add self + end + end + + redef fun on_event(event) + do + super + + if event isa ButtonPressEvent then + var sender = event.sender + if sender == but_test_notif then + notify("Test Notification", "Some content\nmultiline", 5) + else if sender == but_test_checkin then + app.on_check_in + else if sender == but_test_checkout then + app.on_check_out + else if sender == but_test_menu then + app.request_menu + end + end + end +end diff --git a/contrib/benitlux/src/client/features/push.nit b/contrib/benitlux/src/client/features/push.nit new file mode 100644 index 0000000..3006616 --- /dev/null +++ b/contrib/benitlux/src/client/features/push.nit @@ -0,0 +1,222 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Push notification support +module push + +import app::http_request + +import client + +redef class App + redef fun on_log_in + do + super + #(new PushHttpRequest("push/check_token?token={app.token}")).start + end + + # Names of the known users currently on location + var users_on_location = new Set[String] + + # Should we show a daily notification when new beers are available? + fun notify_on_new_beers: Bool + do return app.data_store["notify_on_new_beers"].as(nullable Bool) or else true + + # Should we show a daily notification when new beers are available? + fun notify_on_new_beers=(value: Bool) do app.data_store["notify_on_new_beers"] = value + + # Should we show a daily notification of the menu? + fun notify_menu_daily: Bool + do return app.data_store["notify_menu_daily"].as(nullable Bool) or else false + + # Should we show a daily notification of the menu? + fun notify_menu_daily=(value: Bool) do app.data_store["notify_menu_daily"] = value + + # Should we show a notification when friends check in at the location? + fun notify_on_checkins: Bool + do return app.data_store["notify_on_checkins"].as(nullable Bool) or else true + + # Should we show a notification when friends check in at the location? + fun notify_on_checkins=(value: Bool) do app.data_store["notify_on_checkins"] = value +end + +# Open push notification request +class PushHttpRequest + super BenitluxHttpRequest + + redef fun on_fail(error) + do + if app.user == null then return + + super + + print_error "{class_name}: on_fail {error}" + + var t = new PushHttpRequest("push/?token={app.token}") + t.delay = 10.0 + t.start + end + + redef fun on_load(data) + do + if app.user == null then return + + var delay = 0.0 + if data isa Pushable then + data.apply_push_if_desired + else if data isa BenitluxError then + # TODO if forbidden ask for a new token + delay = 5.0*60.0 + else + print_error "{class_name}: Received {data or else "null"}" + end + + var t = new PushHttpRequest("push/?token={app.token}") + t.delay = delay + t.start + end +end + +# --- +# Objects sent from the server to the client + +# Objects sent as push notifications by the server +interface Pushable + # Act on this push notification + fun apply_push do print_error "Unimplemented `apply_push` on {class_name}" + + # Consider to `apply_push` if the user preferences wants to + fun apply_push_if_desired do apply_push +end + +redef class CheckinReport + super Pushable + + # Flattened array of the name of users + var user_names: Array[String] = [for u in users do u.name] is lazy + + redef fun apply_push_if_desired + do + if not app.notify_on_checkins then return + + var there_is_a_new_user = false + for new_users in user_names do + if not app.users_on_location.has(new_users) then + there_is_a_new_user = true + break + end + end + + app.users_on_location.clear + app.users_on_location.add_all user_names + + # Apply only if there is someone new on location + if there_is_a_new_user then super + end + + redef fun apply_push + do + if users.is_empty then + #app.notif_push.cancel + #self.cancel(tag, (int)id); + return + end + + var title = "TTB!".t + var names = [for user in users do user.name] + var content = "From %0".t.format(names.join(", ")) + + notify(title, content, 1) + end +end + +redef class DailyNotification + super Pushable + + redef fun apply_push_if_desired + do + if app.notify_menu_daily then + super + return + end + + if app.notify_on_new_beers then + for beer in beers do + if beer.is_new then + super + return + end + end + end + end + + redef fun apply_push + do + var title = if beers.has_new_beers then + "New beers are on the menu".t + else "Beer Menu".t + + var content = beers.beers_to_notification + notify(title, content, 3) + end +end + +# --- +# UI + +redef class UserWindow + + private var layout_push_options = new VerticalLayout(parent=layout) + + private var lbl_push_options_title = new Label(parent=layout_push_options, + text="Notifications options".t) + + private var chk_notify_on_new_beers = new CheckBox(parent=layout_push_options, + text="Notify when there are new beers".t) + + private var chk_notify_menu_daily = new CheckBox(parent=layout_push_options, + #text="Show the menu every work day?".t) + text="Show the menu every work day".t) + + private var chk_notify_on_checkins = new CheckBox(parent=layout_push_options, + text="Notify when a friend checks in".t) + + init + do + lbl_push_options_title.size = 1.5 + chk_notify_on_new_beers.is_checked = app.notify_on_new_beers + chk_notify_menu_daily.is_checked = app.notify_menu_daily + chk_notify_on_checkins.is_checked = app.notify_on_checkins + + for c in [chk_notify_menu_daily, chk_notify_on_new_beers, chk_notify_on_checkins] do + c.observers.add self + end + end + + redef fun on_event(event) + do + super + + if event isa ToggleEvent then + var sender = event.sender + if sender == chk_notify_on_new_beers then + app.notify_on_new_beers = sender.is_checked + else if sender == chk_notify_menu_daily then + app.notify_menu_daily = sender.is_checked + else if sender == chk_notify_on_checkins then + app.notify_on_checkins = sender.is_checked + end + end + end +end diff --git a/contrib/benitlux/src/client/features/translations.nit b/contrib/benitlux/src/client/features/translations.nit new file mode 100644 index 0000000..02deb61 --- /dev/null +++ b/contrib/benitlux/src/client/features/translations.nit @@ -0,0 +1,103 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and + +# Support for translating the app to different languages, implements French +module translations + +redef class Text + # Translate `self` according to the current language `sys.lang` + fun t: String + do + var lang = sys.lang_map + if lang == null then return to_s + + if lang.keys.has(self) then return lang[self] + + print "Translation miss ({sys.lang}): {self}" + return to_s + end +end + +redef class Sys + # Name of the language in use + var lang = "C" + + # Translation map for the language in use + var lang_map: nullable Map[Text, String] = null +end + +# Set French as the current language +fun set_fr +do + var map = new Map[Text, String] + + # Home views + map["Welcome %0"] = "Bienvenue %0" + map["Welcome"] = "Bienvenue" + map["Beer Menu"] = "Menu de bières" + map["View all"] = "Menu complet" + map["Preferences"] = "Préférences" + map["Friends"] = "Amis" + map["Manage"] = "Gérer" + map["Events"] = "Événements" + map["Loading..."] = "Chargement..." + map["Login or signup"] = "S'authentifier" + map["On location?"] = "Sur place?" + map["Leaving?"] = "Vous quittez?" + + # User/login views + map["Account options"] = "Options du compte" + map["Share options"] = "Options de partage" + map["Notifications options"] = "Options de notification" + map["Please login"] = "Veuillez vous identifier" + map["Welcome %0!"] = "Bienvenue %0!" + map["Logged in as %0"] = "Connecté en tant que %0" + map["Username"] = "Nom d'utilisateur" + map["Invalid name"] = "Nom d'utilisateur invalide" + map["Password"] = "Mot de passe" + map["Passwords must be composed of at least 6 characters."] = "Le mot de passe doit avoir au moins 6 charactères." + map["Email"] = "Courriel" + map["Login"] = "Se connecter" + map["Logout"] = "Se déconnecter" + map["Signup"] = "Créer un compte" + + # Social views + map["Follow"] = "Suivre" + map["Unfollow"] = "Ne plus suivre" + map["Search"] = "Rechercher" + map["Favorites: %0"] = "Favoris: %0" + map["No favorites yet"] = "Pas de favoris" + map["List followed"] = "Personnes suivies" + map["List followers"] = "Personnes vous suivant" + + # Beer views + map["Review %0"] = "Évaluer %0" + map["%0★ %1 reviews"] = "%0★ %1 avis" + map["No reviews yet"] = "Aucun avis" + map[", friends: %0☆ %1 reviews"] = ", amis: %0☆ %1 avis" + map[" (New)"] = " (Nouveau)" + map["Similar to %0."] = "Similaire à %0." + map["Favorite beer on the menu."] = "Bière préférée sur le menu." + map["Favorite of %0"] = "Préférée de %0" + + # Preferences + map["Notify when a friend checks in"] = "Lorsqu'un ami est sur place" + map["Show the menu every work day"] = "Menu journalier en semaine" + map["Notify when there are new beers"] = "Lorsqu'il y a de nouvelles bières" + map["Share checkins with your friends"] = "Partager lorsque vous êtes sur place" + map["Passing by the Benelux?"] = "De passage au Bénélux?" + + # Update Sys + sys.lang = "fr" + sys.lang_map = map +end diff --git a/contrib/benitlux/src/client/ios.nit b/contrib/benitlux/src/client/ios.nit new file mode 100644 index 0000000..9601202 --- /dev/null +++ b/contrib/benitlux/src/client/ios.nit @@ -0,0 +1,147 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# iOS variant using a button to check in/out and local notifications +module ios + +import ::ios +intrude import app::ui + +import client +import push +import checkins + +redef class HomeWindow + init + do + title = "Benitlux" + update_checkin_text + checkin_button.observers.add self + end + + # TODO hide when not logged in + private var layout_login_checkin = new HorizontalLayout(parent=layout_user) + private var checkin_label = new Label(parent=layout_login_checkin) + private var checkin_button = new Button(parent=layout_login_checkin) + + redef fun on_event(event) + do + super + + if event isa ButtonPressEvent then + var sender = event.sender + if sender == checkin_button then + if app.currently_on_location then + app.on_check_out + else app.on_check_in + end + end + end + + private fun update_checkin_text + do + if app.currently_on_location then + checkin_label.text = "Leaving?".t + checkin_button.text = "Check out".t + else + checkin_label.text = "On location?".t + checkin_button.text = "Check in".t + end + end +end + +redef class App + redef fun on_check_in + do + super + var window = window + if window isa HomeWindow then window.update_checkin_text + end + + redef fun on_check_out + do + super + var window = window + if window isa HomeWindow then window.update_checkin_text + end + + redef fun did_finish_launching_with_options + do + ui_application.register_user_notification_settings + return super + end +end + +redef class UserWindow + init do title = "Preferences".t +end + +redef class BeersWindow + init do title = "Beers".t +end + +redef class SocialWindow + init do title = "People".t +end + +# --- Notifications + +redef fun notify(title, content, id) +do native_notify(title.to_nsstring, content.to_nsstring) + +private fun native_notify(title, content: NSString) in "ObjC" `{ + UILocalNotification* notif = [[UILocalNotification alloc] init]; + notif.alertTitle = title; + notif.alertBody = content; + notif.timeZone = [NSTimeZone defaultTimeZone]; + [[UIApplication sharedApplication] presentLocalNotificationNow: notif]; +`} + +redef class UIApplication + + # Register this app to display notifications + private fun register_user_notification_settings + in "ObjC" `{ + if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]){ + [self registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]; + } + `} +end + +# --- +# Shorten labels + +redef class Label + # Ellipsize `text` so it fits within `max_length` characters + # + # FIXME Remove this when labels are correctly ellipsized on iOS. + redef fun text=(text) + do + if text == null then + super + return + end + + var max_length = 50 + if parent isa HorizontalLayout and parent.parent isa BeerView then + # This is the name of a beer, remember its a hack + max_length = 20 + end + + if text.length > max_length then + text = text.substring(0, max_length - 3).to_s + "..." + end + super text + end +end diff --git a/contrib/benitlux/src/client/views/beer_views.nit b/contrib/benitlux/src/client/views/beer_views.nit new file mode 100644 index 0000000..54ad9e9 --- /dev/null +++ b/contrib/benitlux/src/client/views/beer_views.nit @@ -0,0 +1,329 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Window to list beers and other beer-related views +module beer_views + +import base + +# View about a beer, with its name, description and rating +class BeerView + super VerticalLayout + super ItemView + + autoinit beer_info, parent + + # Beer information + var beer_info: BeerAndRatings + + # Buttons to realize the rating buttons + var star_buttons = new Array[StarButton] + + # Layout of the first line with the name and `star_buttons` + var top_line_layout = new HorizontalLayout(parent=self) + + init + do + var lbl_name = new Label(parent=top_line_layout, text=beer_info.beer.name) + lbl_name.size = 1.25 + + var desc = beer_info.beer.desc + if beer_info.is_new then desc += " (New)".t + var lbl_desc = new Label(parent=self, text=desc) + + var lbl_stats = new Label(parent=self, text=beer_info.rating_text) + lbl_stats.size = 0.5 + + var badges = beer_info.badges + if badges != null then + var lbl_comment = new Label(parent=self, text=badges.join(" ")) + lbl_comment.size = 0.5 + end + + var rating = beer_info.user_rating or else 0 + setup_stars rating + end + + # Prepare and display the rating controls + fun setup_stars(rating: Int) + do + var l_stars = new HorizontalLayout(parent=top_line_layout) + + for i in [1..5] do + var but = new StarButton(beer_info.beer, i, i <= rating, parent=l_stars) + but.size = 1.5 + but.observers.add self + star_buttons.add but + end + end + + redef fun on_event(event) + do + assert event isa ButtonPressEvent + + var sender = event.sender + if sender isa StarButton then + on_review sender.rating + end + end + + # Post a user review + fun on_review(rating: Int) + do + var beer_id = beer_info.beer.id + (new ReviewAction(app.window, "rest/review?token={app.token}&beer={beer_id}&rating={rating}")).start + + # Update UI + var i = 1 + for but in star_buttons do + but.on = i <= rating + i += 1 + end + end +end + +# Beers pane listing the available beers +class BeersWindow + super Window + + private var layout = new VerticalLayout(parent=self) + private var list_beers = new ListLayout(parent=layout) + + init + do + if debug then print "BenitluxWindow::init" + + action_list_beers + end + + # Send HTTP request to list beers + fun action_list_beers + do (new ListBeersAction(self, "rest/list?token={app.token}")).start +end + +# --- +# Customized buttons + +# View to describe and rate a eer +class RatingView + super View + + autoinit beer, init_rating, parent, enabled + + # Beer id + var beer: Beer + + # Previous rating, 0 for none + var init_rating: Int + + redef fun parent=(layout) do end + + redef fun enabled=(value) do end +end + +# Button with a star, filled or not, for rating beers +class StarButton + super Button + + autoinit beer, rating, on, parent, enabled + + # Info on the beer to rate + var beer: Beer + + # Rating of `beer` + var rating: Int + + # Set if the star is filled + fun on=(on: Bool) is autoinit do text = if on then "★" else "☆" +end + +redef class BeerAndRatings + # Text version of the ratings + fun rating_text: String + do + var txt = new Array[String] + + var global = global + if global != null and global.count > 0 then + txt.add "%0★ %1 reviews".t.format(global.average.to_precision(1), global.count) + else txt.add "No reviews yet".t + + var local = followed + if local != null and local.count > 0 then + txt.add ", friends: %0☆ %1 reviews".t.format(local.average.to_precision(1), local.count) + end + + return txt.join + end +end + +redef class Beer + # Capitalize first letter for a prettier display + redef fun desc + do + var desc = super + if desc.length == 0 then return desc + + var first_letter = desc.first.to_upper + return first_letter.to_s + desc.substring_from(1) + end +end + +# Comparator of beers +class BeerComparator + super Comparator + + redef type COMPARED: BeerAndRatings + + redef fun compare(a, b) do return value_of(a) <=> value_of(b) + + private fun value_of(beer: COMPARED): Float + do + var max = 0.0 + var value = 0.0 + + var rating = beer.user_rating + if rating != null then + max += 20.0 + value += rating.to_f * 4.0 + end + + var followed = beer.followed + if followed != null then + max += 10.0 + value += followed.average * 2.0 + end + + var global = beer.global + if global != null then + max += 5.0 + value += global.average + end + + return (max - value)/max + end +end + +# Async request to submit a review +class ReviewAction + super WindowHttpRequest + + redef fun on_load(res) + do + if intercept_error(res) then return + end +end + +# Async request to update the beer list +class ListBeersAction + super WindowHttpRequest + + redef type W: BeersWindow + + redef fun on_load(beers) + do + window.layout.remove window.list_beers + window.list_beers = new ListLayout(parent=window.layout) + + if intercept_error(beers) then return + + if not beers isa Array[BeerAndRatings] then + app.feedback "Communication Error".t + return + end + + # Sort beers per preference + var comparator = new BeerComparator + comparator.sort beers + + # Populate the list + for beer_and_rating in beers do + var view = new BeerView(beer_and_rating, parent=window.list_beers) + end + end +end + +redef class BestBeerBadge + redef fun to_s do return "Favorite beer on the menu.".t +end + +redef class FavoriteBeerBadge + redef fun to_s do return "Favorite of %0.".t.format(users.join(", ", " & ")) +end + +redef class SimilarBeerBadge + redef fun to_s do return "Similar to %0.".t.format(beers.join(", ", " & ")) +end + +redef class Array[E] + # Pretty compressed list of this list of beer as a pseudo diff + # + # Require: `self isa Array[BeerAndRatings]` + fun beers_to_notification: String + do + assert self isa Array[BeerAndRatings] + + # Sort beers per preference + var comparator = new BeerComparator + comparator.sort self + + # Organize the notification line per line + # First the new beers, then the fixed one. + var lines = new Array[String] + var fix_beers = new Array[String] + for bar in self do + var beer = bar.beer + if bar.is_new then + lines.add "+ {beer.name}: {beer.desc}" + else fix_beers.add beer.name + end + + # Show a few fixed beers per line + if fix_beers.not_empty then + var line = new FlatBuffer + line.append "= " + for i in fix_beers.length.times, beer in fix_beers do + + if i > 0 then line.append ", " + + var l = line.length + beer.length + if l < 42 then # Very approximate width of a notification on Android + line.append beer + continue + end + + lines.add line.to_s + + line = new FlatBuffer + line.append "= " + line.append beer + end + + lines.add line.to_s + end + + return lines.join("\n") + end + + # Does `self` has a new beer? + # + # Require: `self isa Array[BeerAndRatings]` + fun has_new_beers: Bool + do + assert self isa Array[BeerAndRatings] + + for beer in self do if beer.is_new then return true + return false + end +end diff --git a/contrib/benitlux/src/client/views/home_views.nit b/contrib/benitlux/src/client/views/home_views.nit new file mode 100644 index 0000000..c991f3c --- /dev/null +++ b/contrib/benitlux/src/client/views/home_views.nit @@ -0,0 +1,211 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Main home window +module home_views + +import beer_views +import social_views +import user_views + +redef class App + redef fun on_create + do + if debug then print "App::on_create" + + # Create the main window + show_home + super + end + + # Show the home/main windows + fun show_home + do + var window = new HomeWindow + window.refresh + push_window window + end + + redef fun on_log_in + do + super + + # Send back to the home window when logging in + if not window isa HomeWindow then pop_window + end +end + +# Social pane with networking features +class HomeWindow + super Window + + private var layout = new ListLayout(parent=self) + + # Cut-point for the iOS adaptation + var layout_user = new VerticalLayout(parent=layout) + private var layout_login = new HorizontalLayout(parent=layout_user) + private var lbl_login_status = new Label(parent=layout_login, text="Welcome".t, size=1.5) + private var but_login = new Button(parent=layout_login, text="Login or signup".t) + private var but_preferences = new Button(parent=layout_login, text="Preferences".t) + + private var layout_beers = new VerticalLayout(parent=layout) + var layout_beers_title = new HorizontalLayout(parent=layout_beers) + var title_beers = new SectionTitle(parent=layout_beers_title, text="Beer Menu".t, size=1.5) + private var beer_button = new Button(parent=layout_beers_title, text="View all".t) + private var beer_list = new VerticalLayout(parent=layout_beers) + private var beer_temp_lbl = new Label(parent=beer_list, text="Loading...".t) + + private var layout_social = new VerticalLayout(parent=layout) + private var social_header = new HorizontalLayout(parent=layout_social) + private var social_title = new SectionTitle(parent=social_header, text="Friends".t, size=1.5) + private var social_button = new Button(parent=social_header, text="Manage".t) + private var social_list = new VerticalLayout(parent=layout_social) + private var social_temp_lbl = new Label(parent=social_list, text="Loading...".t) + + private var layout_news = new VerticalLayout(parent=layout) + private var news_header = new HorizontalLayout(parent=layout_news) + private var news_title = new SectionTitle(parent=news_header, text="Events".t, size=1.5) + #private var news_button = new Button(parent=news_header, text="Open website") # TODO + private var news_label = new Label(parent=layout_news, text="Bière en cask le jeudi!") + + init + do + for c in [but_login, but_preferences, beer_button, social_button] do + c.observers.add self + end + end + + redef fun on_resume do refresh + + # Refresh content of this page + fun refresh + do + if not app.restored then return + + layout_login.clear + if app.user != null then + # Logged in + lbl_login_status.parent = layout_login + but_preferences.parent = layout_login + lbl_login_status.set_welcome + else + but_login.parent = layout_login + but_preferences.parent = layout_login + end + + # Fill beers + (new ListDiffAction(self, "rest/since?token={app.token}")).start + + # Fill people + (new HomeListPeopleAction(self, "rest/friends?token={app.token}")).start + + # Check if token is still valid + (new CheckTokenAction(self, "rest/check_token?token={app.token}")).start + end + + redef fun on_event(event) + do + if debug then print "BenitluxWindow::on_event {event}" + + if event isa ButtonPressEvent then + var sender = event.sender + if sender == but_preferences then + app.push_window new UserWindow + return + else if sender == but_login then + app.push_window new SignupWindow + return + else if sender == beer_button then + app.push_window new BeersWindow + return + else if sender == social_button then + app.push_window new SocialWindow + return + #else if sender == news_button then + # TODO open browser? + end + end + + super + end +end + +# `Label` used in section headers +class SectionTitle super Label end + +# Async request to update the beer list on the home screen +class ListDiffAction + super WindowHttpRequest + + redef type W: HomeWindow + + redef fun on_load(beers) + do + window.layout_beers.remove window.beer_list + window.beer_list = new VerticalLayout(parent=window.layout_beers) + + if intercept_error(beers) then return + + if not beers isa Array[BeerAndRatings] then + app.feedback "Communication Error".t + return + end + + # Sort beers per preference + var comparator = new BeerComparator + comparator.sort beers + + var max_beers = 6 + while beers.length > max_beers do beers.pop + + for bar in beers do + var view = new BeerView(bar, parent=window.beer_list) + end + end +end + +# Async request to list users +class HomeListPeopleAction + super WindowHttpRequest + + redef type W: HomeWindow + + redef fun on_load(users) + do + window.layout_social.remove window.social_list + window.social_list = new VerticalLayout(parent=window.layout_social) + + if intercept_error(users) then return + + if users isa Array[UserAndFollowing] then for uaf in users do + var view = new PeopleView(uaf, true, parent=window.social_list) + end + end +end + +# Async request to check if `app.token` is still valid +class CheckTokenAction + super WindowHttpRequest + + redef type W: HomeWindow + + redef fun on_load(res) do intercept_error(res) +end + +# Today's date as a `String` +fun today: String +do + var tm = new Tm.localtime + return "{tm.year+1900}-{tm.mon+1}-{tm.mday}" +end diff --git a/contrib/benitlux/src/client/views/social_views.nit b/contrib/benitlux/src/client/views/social_views.nit new file mode 100644 index 0000000..35cb6bd --- /dev/null +++ b/contrib/benitlux/src/client/views/social_views.nit @@ -0,0 +1,201 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Window to list beers and other beer-related views +module social_views + +import base + +# Social pane with networking features +class SocialWindow + super Window + + private var layout = new VerticalLayout(parent=self) + + private var list_search = new ListLayout(parent=layout) + + private var layout_header = new VerticalLayout(parent=list_search) + private var layout_search = new HorizontalLayout(parent=layout_header) + private var txt_query = new TextInput(parent=layout_search) + private var but_search = new Button(parent=layout_search, text="Search".t) + + private var layout_list = new HorizontalLayout(parent=layout_header) + private var but_followed = new Button(parent=layout_list, text="List followed".t) + private var but_followers = new Button(parent=layout_list, text="List followers".t) + + init + do + for c in [but_search, but_followed, but_followers] do + c.observers.add self + end + + # Load friends and suggestions + (new ListUsersAction(self, "rest/friends?token={app.token}&n=16")).start + end + + redef fun on_event(event) + do + if debug then print "BenitluxWindow::on_event {event}" + + if event isa ButtonPressEvent then + var sender = event.sender + if sender == but_search then + search + else if sender == but_followed then + var cmd = "rest/followed?token={app.token}" + (new ListUsersAction(self, cmd)).start + else if sender == but_followers then + var cmd = "rest/followers?token={app.token}" + (new ListUsersAction(self, cmd)).start + end + end + + super + end + + # Execute search with `txt_query.text` + fun search + do + var query = txt_query.text + if query == null or query.is_empty then return + + var res = "rest/search?token={app.token}&query={query}&offset=0" + (new ListUsersAction(self, res)).start + end + + # Fill `list_search` with views for each of `users` + fun list_users(users: Array[UserAndFollowing]) + do + for uaf in users do + var view = new PeopleView(uaf, false, parent=list_search) + end + end +end + +# View to describe, and follow a person +class PeopleView + super VerticalLayout + super ItemView + + autoinit user_and_following, home_window_mode, parent + + # Description of the user + var user_and_following: UserAndFollowing + + # Toggle tweaks for the home window where the is no "unfollow" buttons + var home_window_mode: Bool + + init + do + var user = user_and_following.user + + var layout_top_line = new HorizontalLayout(parent=self) + var lbl_name = new Label(parent=layout_top_line, text=user.name) + + if app.user != null then + + # Show unfollow button if not on the home screen + if not home_window_mode or not user_and_following.following then + var but = new FollowButton(user.id, user_and_following.following, user_and_following.followed, parent=layout_top_line) + but.observers.add self + end + end + + var favs = if not user_and_following.favs.is_empty then + "Favorites: %0".t.format(user_and_following.favs) + else "No favorites yet".t + var lbl_desc = new Label(parent=self, text=favs, size=0.5) + end +end + +# Button to follow or unfollow a user +class FollowButton + super Button + + autoinit followed_id, following, followed_by, parent, enabled, text + + # Id of the user to be followd/unfollow + var followed_id: Int + + # Does the local user already follows `followed_id` + var following: Bool + + # Does `followed_id` already follows the local user + var followed_by: Bool + + # Update the visible text according to `following` + fun update_text do text = if following then "Unfollow".t else "Follow".t + + init do update_text + + redef fun on_event(event) + do + assert event isa ButtonPressEvent + var cmd = "rest/follow?token={app.token}&user_to={followed_id}&follow={not following}" + enabled = false + text = "Updating...".t + (new FollowAction(app.window, cmd, self)).start + end +end + +# Async request to receive and display a list of users +# +# This is used by many features of the social window: +# search, list followed and list followers. +class ListUsersAction + super WindowHttpRequest + + redef type W: SocialWindow + + init do affected_views.add_all([window.but_search, window.but_followed, window.but_followers]) + + redef fun on_load(users) + do + window.layout.remove window.list_search + window.list_search = new ListLayout(parent=window.layout) + window.layout_header.parent = window.list_search + + if intercept_error(users) then return + + if users isa Array[UserAndFollowing] then window.list_users users + end +end + +# Async request to follow or unfollow a user +class FollowAction + super WindowHttpRequest + + private var button: FollowButton + init do affected_views.add(button) + + redef fun on_load(res) + do + if intercept_error(res) then return + end + + redef fun after + do + button.following = not button.following + button.update_text + button.enabled = true + + super + end + + redef fun before + do + button.enabled = false + super + end +end diff --git a/contrib/benitlux/src/client/views/user_views.nit b/contrib/benitlux/src/client/views/user_views.nit new file mode 100644 index 0000000..e45fcb5 --- /dev/null +++ b/contrib/benitlux/src/client/views/user_views.nit @@ -0,0 +1,215 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# User preference window and other user-related view +module user_views + +import base + +redef class Label + # Update the content of `lbl_welcome` + fun set_user_name + do + var name = app.user + self.text = if name != null then + "Logged in as %0".t.format(name) + else "Not logged in".t + end + + # Set `text` to welcome an authentified user or invite to authentify + fun set_welcome + do + var name = app.user + self.text = if name != null then + "Welcome %0".t.format(name) + else "" + end +end + +# User preference window +class UserWindow + super Window + + # Main window layout + var layout = new ListLayout(parent=self) + + private var layout_user_options = new VerticalLayout(parent=layout) + + private var lbl_user_options_title = new Label(parent=layout_user_options, + text="Account options".t) + + private var lbl_welcome = new Label(parent=layout_user_options) + private var but_logout = new Button(parent=layout_user_options, text="Logout".t) + + # Refesh displayed text + fun refresh + do + lbl_user_options_title.size = 1.5 + lbl_welcome.set_user_name + but_logout.enabled = app.user != null + end + + init + do + but_logout.observers.add self + refresh + end + + redef fun on_event(event) + do + if event isa ButtonPressEvent then + var sender = event.sender + if sender == but_logout then + app.user = null + app.token = "none" + app.on_log_in + refresh + end + end + + super + end +end + +# Window for signing up a new user or logging in +class SignupWindow + super Window + + # Main window layout + var layout = new ListLayout(parent=self) + + private var lbl_welcome = new Label(parent=layout, text="Welcome") + + # Name + private var name_line = new HorizontalLayout(parent=layout) + private var lbl_name = new Label(parent=name_line, text="Username".t) + private var txt_name = new TextInput(parent=name_line, text=app.user) + + # Pass + private var pass_line = new HorizontalLayout(parent=layout) + private var lbl_pass = new Label(parent=pass_line, text="Password".t) + private var txt_pass = new TextInput(parent=pass_line, is_password=true) + private var lbl_pass_desc = new Label(parent=layout, + text="Passwords must be composed of at least 6 characters.".t) + + private var but_login = new Button(parent=layout, text="Login".t) + + # Email + private var email_line = new HorizontalLayout(parent=layout) + private var lbl_email = new Label(parent=email_line, text="Email".t) + private var txt_email = new TextInput(parent=email_line) + + private var but_signup = new Button(parent=layout, text="Signup".t) + + private var lbl_feedback = new Label(parent=layout, text="") + + init + do + lbl_pass_desc.size = 0.5 + + for c in [but_login, but_signup] do + c.observers.add self + end + end + + redef fun on_event(event) + do + if debug then print "BenitluxWindow::on_event {event}" + + if event isa ButtonPressEvent then + var sender = event.sender + if sender == but_login or sender == but_signup then + + var name = txt_name.text + if name == null or not name.name_is_ok then + feedback "Invalid name".t + return + end + + var pass = txt_pass.text + if pass == null or not pass.pass_is_ok then + feedback "Invalid password".t + return + end + + if sender == but_login then + (new LoginOrSignupAction(self, "rest/login?name={name}&pass={pass.pass_hash}")).start + else if sender == but_signup then + var email = txt_email.text + if email == null or email.is_empty then + feedback "Invalid email".t + return + end + + (new LoginOrSignupAction(self, "rest/signup?name={name}&pass={pass.pass_hash}&email={email}")).start + end + end + end + + super + end + + # Show lasting feedback to the user in a label + fun feedback(text: String) do lbl_feedback.text = text +end + +# --- +# Async RESTful actions + +# Async request for login in or signing up +class LoginOrSignupAction + super WindowHttpRequest + + redef type W: SignupWindow + + init do affected_views.add_all([window.but_login, window.but_signup]) + + redef fun on_load(res) + do + if intercept_error(res) then return + + if not res isa LoginResult then + on_fail new Error("Server sent unexpected data {res or else "null"}") + return + end + + app.token = res.token + app.user = res.user.name + + app.on_log_in + end +end + +# Async request for signing up +class SignupAction + super WindowHttpRequest + + redef type W: SignupWindow + + init do affected_views.add_all([window.but_signup]) + + redef fun on_load(res) + do + if intercept_error(res) then return + + if not res isa LoginResult then + on_fail new Error("Server sent unexpected data {res or else "null"}") + return + end + + app.token = res.token + app.user = res.user.name + app.on_log_in + end +end diff --git a/contrib/benitlux/src/benitlux_controller.nit b/contrib/benitlux/src/server/benitlux_controller.nit similarity index 85% rename from contrib/benitlux/src/benitlux_controller.nit rename to contrib/benitlux/src/server/benitlux_controller.nit index 5f5513a..5649345 100644 --- a/contrib/benitlux/src/benitlux_controller.nit +++ b/contrib/benitlux/src/server/benitlux_controller.nit @@ -133,6 +133,16 @@ class BenitluxRESTAction 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 @@ -322,6 +332,57 @@ redef class Sys 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 @@ -348,7 +409,7 @@ redef class HttpResponse 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 @@ -356,7 +417,7 @@ redef class HttpResponse 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 @@ -364,7 +425,7 @@ redef class HttpResponse 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 @@ -372,6 +433,6 @@ redef class HttpResponse 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 diff --git a/contrib/benitlux/src/benitlux_daily.nit b/contrib/benitlux/src/server/benitlux_daily.nit similarity index 100% rename from contrib/benitlux/src/benitlux_daily.nit rename to contrib/benitlux/src/server/benitlux_daily.nit diff --git a/contrib/benitlux/src/benitlux_db.nit b/contrib/benitlux/src/server/benitlux_db.nit similarity index 93% rename from contrib/benitlux/src/benitlux_db.nit rename to contrib/benitlux/src/server/benitlux_db.nit index 81ec6bf..c899ede 100644 --- a/contrib/benitlux/src/benitlux_db.nit +++ b/contrib/benitlux/src/server/benitlux_db.nit @@ -85,7 +85,7 @@ class BenitluxDB 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` @@ -93,26 +93,35 @@ class BenitluxDB # 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 diff --git a/contrib/benitlux/src/benitlux_social.nit b/contrib/benitlux/src/server/benitlux_social.nit similarity index 100% rename from contrib/benitlux/src/benitlux_social.nit rename to contrib/benitlux/src/server/benitlux_social.nit diff --git a/contrib/benitlux/src/benitlux_view.nit b/contrib/benitlux/src/server/benitlux_view.nit similarity index 100% rename from contrib/benitlux/src/benitlux_view.nit rename to contrib/benitlux/src/server/benitlux_view.nit diff --git a/contrib/benitlux/src/benitlux_web.nit b/contrib/benitlux/src/server/server.nit similarity index 80% rename from contrib/benitlux/src/benitlux_web.nit rename to contrib/benitlux/src/server/server.nit index d5bab54..f6d5d41 100644 --- a/contrib/benitlux/src/benitlux_web.nit +++ b/contrib/benitlux/src/server/server.nit @@ -15,7 +15,7 @@ # limitations under the License. # Web server for Benitlux -module benitlux_web +module server import benitlux_model import benitlux_view @@ -25,6 +25,9 @@ import benitlux_restful # 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) @@ -40,10 +43,14 @@ vh.routes.add new Route("/rest/", new BenitluxRESTAction(db)) 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 diff --git a/contrib/friendz/src/friendz.nit b/contrib/friendz/src/friendz.nit index c0554fe..762eb1d 100644 --- a/contrib/friendz/src/friendz.nit +++ b/contrib/friendz/src/friendz.nit @@ -1609,22 +1609,10 @@ redef class App 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) diff --git a/contrib/tinks/src/client/linux_client.nit b/contrib/tinks/src/client/linux_client.nit index 6ef6b15..bd60702 100644 --- a/contrib/tinks/src/client/linux_client.nit +++ b/contrib/tinks/src/client/linux_client.nit @@ -54,8 +54,7 @@ redef class App # 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 diff --git a/contrib/tinks/src/client/tinks_vr.nit b/contrib/tinks/src/client/tinks_vr.nit new file mode 100644 index 0000000..0e2a300 --- /dev/null +++ b/contrib/tinks/src/client/tinks_vr.nit @@ -0,0 +1,22 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# VR mode for Android with Google Cardboard +# +# This version is not playable and very laggy as it is not modified +# or optimized in any way for VR. +# This module is made available as a minimal example of a VR game. +module tinks_vr + +import gamnit::vr diff --git a/contrib/tinks/src/game/framework.nit b/contrib/tinks/src/game/framework.nit index 49c74b0..9472da6 100644 --- a/contrib/tinks/src/game/framework.nit +++ b/contrib/tinks/src/game/framework.nit @@ -41,7 +41,7 @@ class TGame 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 diff --git a/contrib/tnitter/package.ini b/contrib/tnitter/package.ini index a01d725..4d7720f 100644 --- a/contrib/tnitter/package.ini +++ b/contrib/tnitter/package.ini @@ -1,13 +1,13 @@ [package] name=tnitter -tags=web +tags=web,mobile maintainer=Alexis Laferrière 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 diff --git a/contrib/tnitter/src/action.nit b/contrib/tnitter/src/action.nit index ad41a71..60ee384 100644 --- a/contrib/tnitter/src/action.nit +++ b/contrib/tnitter/src/action.nit @@ -266,14 +266,14 @@ class TnitterREST 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 diff --git a/contrib/tnitter/src/push.nit b/contrib/tnitter/src/push.nit index cdb2945..bc47789 100644 --- a/contrib/tnitter/src/push.nit +++ b/contrib/tnitter/src/push.nit @@ -46,7 +46,7 @@ redef class DB # 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` diff --git a/contrib/tnitter/src/tnitter_app.nit b/contrib/tnitter/src/tnitter_app.nit index fccad93..7a47218 100644 --- a/contrib/tnitter/src/tnitter_app.nit +++ b/contrib/tnitter/src/tnitter_app.nit @@ -42,7 +42,7 @@ redef class App redef fun on_create do # Create the main window - window = new TnitterWindow + push_window new TnitterWindow super end end diff --git a/contrib/tnitter/src/tnitter_app_android.nit b/contrib/tnitter/src/tnitter_app_android.nit index 74eb1b6..dc24681 100644 --- a/contrib/tnitter/src/tnitter_app_android.nit +++ b/contrib/tnitter/src/tnitter_app_android.nit @@ -19,8 +19,7 @@ end import tnitter_app -import android::ui -import android::http_request +import android import android::portrait redef class LabelAuthor diff --git a/contrib/xymus_net/.gitignore b/contrib/xymus_net/.gitignore new file mode 100644 index 0000000..b2285d4 --- /dev/null +++ b/contrib/xymus_net/.gitignore @@ -0,0 +1 @@ +xymus.net diff --git a/contrib/xymus_net/Makefile b/contrib/xymus_net/Makefile new file mode 100644 index 0000000..eff98e3 --- /dev/null +++ b/contrib/xymus_net/Makefile @@ -0,0 +1,9 @@ +all: xymus.net + +xymus.net: ../benitlux/src/server/benitlux_restful.nit $(shell ../../bin/nitls -M xymus_net.nit) + ../../bin/nitc -o $@ xymus_net.nit + +../benitlux/src/server/benitlux_restful.nit: + make -C ../benitlux src/server/benitlux_restful.nit + +pre-build: ../benitlux/src/server/benitlux_restful.nit diff --git a/contrib/xymus_net/README.md b/contrib/xymus_net/README.md new file mode 100644 index 0000000..5bb2c31 --- /dev/null +++ b/contrib/xymus_net/README.md @@ -0,0 +1,5 @@ +Web server source and config of xymus.net + +This module acts also as an example to merge multiple `nitcorn` projects into one server. + +See the server online at http://xymus.net/. diff --git a/contrib/xymus_net/package.ini b/contrib/xymus_net/package.ini new file mode 100644 index 0000000..249a39a --- /dev/null +++ b/contrib/xymus_net/package.ini @@ -0,0 +1,11 @@ +[package] +name=xymus_net +tags=web,example +maintainer=Alexis Laferrière +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 diff --git a/lib/nitcorn/examples/src/xymus_net.nit b/contrib/xymus_net/xymus_net.nit similarity index 96% rename from lib/nitcorn/examples/src/xymus_net.nit rename to contrib/xymus_net/xymus_net.nit index 1233db4..726cb6a 100644 --- a/lib/nitcorn/examples/src/xymus_net.nit +++ b/contrib/xymus_net/xymus_net.nit @@ -1,6 +1,6 @@ # This file is part of NIT ( http://www.nitlanguage.org ). # -# Copyright 2014-2015 Alexis Laferrière +# Copyright 2014-2016 Alexis Laferrière # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import privileges # Use actions defined by contribs import tnitter import benitlux::benitlux_controller +import benitlux::benitlux_restful import opportunity::opportunity_controller import nitiwiki::wiki_edit @@ -173,6 +174,7 @@ var vps_vh = new VirtualHost("vps.xymus.net:80") var tnitter_vh = new VirtualHost("tnitter.xymus.net:80") var pep8_vh = new VirtualHost("pep8.xymus.net:80") var benitlux_vh = new VirtualHost("benitlux.xymus.net:80") +var benitlux_admin_vh = new VirtualHost("localhost:8081") var factory = new HttpFactory.and_libevent factory.config.virtual_hosts.add default_vh @@ -180,6 +182,7 @@ factory.config.virtual_hosts.add vps_vh factory.config.virtual_hosts.add tnitter_vh factory.config.virtual_hosts.add pep8_vh factory.config.virtual_hosts.add benitlux_vh +factory.config.virtual_hosts.add benitlux_admin_vh # Ports are open, drop to a low-privileged user if we are root var user_group = new UserGroup("nitcorn", "nitcorn") @@ -228,6 +231,8 @@ benitlux_vh.routes.add new Route("/push/", benitlux_push) benitlux_vh.routes.add new Route("/static/", shared_file_server) benitlux_vh.routes.add new Route(null, benitlux_sub) +benitlux_admin_vh.routes.add new Route(null, new BenitluxAdminAction(benitlux_db)) + # Opportunity service var opportunity = new OpportunityWelcome var opportunity_rest = new OpportunityRESTAction diff --git a/examples/calculator/package.ini b/examples/calculator/package.ini index 80b32cb..8237327 100644 --- a/examples/calculator/package.ini +++ b/examples/calculator/package.ini @@ -1,6 +1,6 @@ [package] name=calculator -tags=example +tags=example,mobile maintainer=Alexis Laferrière license=Apache-2.0 [upstream] diff --git a/examples/calculator/src/android_calculator.nit b/examples/calculator/src/android_calculator.nit index 7fe192c..1d51073 100644 --- a/examples/calculator/src/android_calculator.nit +++ b/examples/calculator/src/android_calculator.nit @@ -16,7 +16,7 @@ module android_calculator import calculator -import android::ui +import android redef class Button init do set_android_style(native, (text or else "?").is_int) diff --git a/examples/calculator/src/calculator.nit b/examples/calculator/src/calculator.nit index 4068f34..c9db316 100644 --- a/examples/calculator/src/calculator.nit +++ b/examples/calculator/src/calculator.nit @@ -38,7 +38,7 @@ redef class App if debug then print "App::on_create" # Create the main window - window = new CalculatorWindow + push_window new CalculatorWindow super end end diff --git a/examples/rosettacode/perlin_noise.nit b/examples/rosettacode/perlin_noise.nit index 0733bbf..a5cd314 100644 --- a/examples/rosettacode/perlin_noise.nit +++ b/examples/rosettacode/perlin_noise.nit @@ -8,69 +8,7 @@ # See: 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) diff --git a/lib/a_star.nit b/lib/a_star.nit index 7b7ce02..cc0cba3 100644 --- a/lib/a_star.nit +++ b/lib/a_star.nit @@ -188,6 +188,14 @@ class Node end end + # Find the closest node accepted by `cond` under `max_cost` + fun find_closest(max_cost: Int, context: PathContext, cond: nullable TargetCondition[N]): nullable N + do + var path = path_to_alts(null, max_cost, context, cond) + if path == null then return null + return path.nodes.last + end + # We customize the serialization process to avoid problems with recursive # serialization engines. These engines, such as `JsonSerializer`, # are at danger to serialize the graph as a very deep tree. diff --git a/lib/android/NitActivity.java b/lib/android/NitActivity.java index 3744988..cbe6ad4 100644 --- a/lib/android/NitActivity.java +++ b/lib/android/NitActivity.java @@ -17,6 +17,7 @@ package nit.app; import android.app.Activity; import android.os.Bundle; +import android.view.KeyEvent; /* * Entry point to Nit applications on Android, redirect most calls to Nit @@ -47,6 +48,11 @@ public class NitActivity extends Activity { protected native void nitOnDestroy(int activity); protected native void nitOnSaveInstanceState(int activity, Bundle savedInstanceState); protected native void nitOnRestoreInstanceState(int activity, Bundle savedInstanceState); + protected native boolean nitOnBackPressed(int activity); + protected native boolean nitOnKeyDown(int activity, int keyCode, KeyEvent event); + protected native boolean nitOnKeyLongPress(int activity, int keyCode, KeyEvent event); + protected native boolean nitOnKeyMultiple(int activity, int keyCode, int count, KeyEvent event); + protected native boolean nitOnKeyUp(int activity, int keyCode, KeyEvent event); /* * Implementation of OS callbacks @@ -108,4 +114,34 @@ public class NitActivity extends Activity { super.onRestoreInstanceState(savedInstanceState); nitOnRestoreInstanceState(nitActivity, savedInstanceState); } + + @Override + public void onBackPressed() { + if (!nitOnBackPressed(nitActivity)) + super.onBackPressed(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return nitOnKeyDown(nitActivity, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return nitOnKeyLongPress(nitActivity, keyCode, event) + || super.onKeyLongPress(keyCode, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return nitOnKeyMultiple(nitActivity, keyCode, count, event) + || super.onKeyMultiple(keyCode, count, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return nitOnKeyUp(nitActivity, keyCode, event) + || super.onKeyUp(keyCode, event); + } } diff --git a/lib/android/README.md b/lib/android/README.md index 47e0cec..b6bcf7e 100644 --- a/lib/android/README.md +++ b/lib/android/README.md @@ -64,7 +64,16 @@ integer as argument. They are applied in the Android manifest as 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 diff --git a/lib/android/android.nit b/lib/android/android.nit index b5aa04f..58e296a 100644 --- a/lib/android/android.nit +++ b/lib/android/android.nit @@ -21,56 +21,5 @@ 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 diff --git a/lib/android/game.nit b/lib/android/game.nit new file mode 100644 index 0000000..2d17054 --- /dev/null +++ b/lib/android/game.nit @@ -0,0 +1,71 @@ +# 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 diff --git a/lib/android/input_events.nit b/lib/android/input_events.nit index 9597eea..5ee9d87 100644 --- a/lib/android/input_events.nit +++ b/lib/android/input_events.nit @@ -18,7 +18,7 @@ module input_events import mnit::input -import android +import android::game in "C header" `{ #include @@ -70,6 +70,8 @@ private extern class NativeAndroidMotionEvent `{AInputEvent *`} `} fun action: AMotionEventAction `{ return AMotionEvent_getAction(self); `} + + fun native_down_time: Int `{ return AMotionEvent_getDownTime(self); `} end private extern class AMotionEventAction `{ int32_t `} @@ -149,6 +151,11 @@ class AndroidMotionEvent return null end end + + # Time when the user originally pressed down to start a stream of position events + # + # The return value is in the `java.lang.System.nanoTime()` time base. + fun down_time: Int do return native.native_down_time end # A pointer event diff --git a/lib/android/key_event.nit b/lib/android/key_event.nit new file mode 100644 index 0000000..b5b39e4 --- /dev/null +++ b/lib/android/key_event.nit @@ -0,0 +1,192 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module key_event + +import platform + +# Java class: android.view.KeyEvent +extern class NativeKeyEvent in "Java" `{ android.view.KeyEvent `} + super JavaObject + + # Java implementation: boolean android.view.KeyEvent.isSystem() + fun is_system: Bool in "Java" `{ + return self.isSystem(); + `} + + # Java implementation: android.view.KeyEvent.setSource(int) + fun set_source(arg0: Int) in "Java" `{ + self.setSource((int)arg0); + `} + + # Java implementation: int android.view.KeyEvent.getMetaState() + fun meta_state: Int in "Java" `{ + return self.getMetaState(); + `} + + # Java implementation: int android.view.KeyEvent.getModifiers() + fun modifiers: Int in "Java" `{ + return self.getModifiers(); + `} + + # Java implementation: int android.view.KeyEvent.getFlags() + fun flags: Int in "Java" `{ + return self.getFlags(); + `} + + # Java implementation: boolean android.view.KeyEvent.hasNoModifiers() + fun has_no_modifiers: Bool in "Java" `{ + return self.hasNoModifiers(); + `} + + # Java implementation: boolean android.view.KeyEvent.hasModifiers(int) + fun has_modifiers(arg0: Int): Bool in "Java" `{ + return self.hasModifiers((int)arg0); + `} + + # Java implementation: boolean android.view.KeyEvent.isAltPressed() + fun is_alt_pressed: Bool in "Java" `{ + return self.isAltPressed(); + `} + + # Java implementation: boolean android.view.KeyEvent.isShiftPressed() + fun is_shift_pressed: Bool in "Java" `{ + return self.isShiftPressed(); + `} + + # Java implementation: boolean android.view.KeyEvent.isSymPressed() + fun is_sym_pressed: Bool in "Java" `{ + return self.isSymPressed(); + `} + + # Java implementation: boolean android.view.KeyEvent.isCtrlPressed() + fun is_ctrl_pressed: Bool in "Java" `{ + return self.isCtrlPressed(); + `} + + # Java implementation: boolean android.view.KeyEvent.isMetaPressed() + fun is_meta_pressed: Bool in "Java" `{ + return self.isMetaPressed(); + `} + + # Java implementation: boolean android.view.KeyEvent.isFunctionPressed() + fun is_function_pressed: Bool in "Java" `{ + return self.isFunctionPressed(); + `} + + # Java implementation: boolean android.view.KeyEvent.isCapsLockOn() + fun is_caps_lock_on: Bool in "Java" `{ + return self.isCapsLockOn(); + `} + + # Java implementation: boolean android.view.KeyEvent.isNumLockOn() + fun is_num_lock_on: Bool in "Java" `{ + return self.isNumLockOn(); + `} + + # Java implementation: boolean android.view.KeyEvent.isScrollLockOn() + fun is_scroll_lock_on: Bool in "Java" `{ + return self.isScrollLockOn(); + `} + + # Java implementation: int android.view.KeyEvent.getAction() + fun action: Int in "Java" `{ + return self.getAction(); + `} + + # Java implementation: boolean android.view.KeyEvent.isCanceled() + fun is_canceled: Bool in "Java" `{ + return self.isCanceled(); + `} + + # Java implementation: android.view.KeyEvent.startTracking() + fun start_tracking in "Java" `{ + self.startTracking(); + `} + + # Java implementation: boolean android.view.KeyEvent.isTracking() + fun is_tracking: Bool in "Java" `{ + return self.isTracking(); + `} + + # Java implementation: boolean android.view.KeyEvent.isLongPress() + fun is_long_press: Bool in "Java" `{ + return self.isLongPress(); + `} + + # Java implementation: int android.view.KeyEvent.getKeyCode() + fun key_code: Int in "Java" `{ + return self.getKeyCode(); + `} + + # Java implementation: java.lang.String android.view.KeyEvent.getCharacters() + fun characters: JavaString in "Java" `{ + return self.getCharacters(); + `} + + # Java implementation: int android.view.KeyEvent.getScanCode() + fun scan_code: Int in "Java" `{ + return self.getScanCode(); + `} + + # Java implementation: int android.view.KeyEvent.getRepeatCount() + fun repeat_count: Int in "Java" `{ + return self.getRepeatCount(); + `} + + # Java implementation: long android.view.KeyEvent.getDownTime() + fun down_time: Int in "Java" `{ + return self.getDownTime(); + `} + + # Java implementation: long android.view.KeyEvent.getEventTime() + fun event_time: Int in "Java" `{ + return self.getEventTime(); + `} + + # Java implementation: char android.view.KeyEvent.getDisplayLabel() + fun display_label: Char in "Java" `{ + return self.getDisplayLabel(); + `} + + # Java implementation: int android.view.KeyEvent.getUnicodeChar() + fun unicode_char: Int in "Java" `{ + return self.getUnicodeChar(); + `} + + # Java implementation: char android.view.KeyEvent.getNumber() + fun number: Char in "Java" `{ + return self.getNumber(); + `} + + # Java implementation: boolean android.view.KeyEvent.isPrintingKey() + fun is_printing_key: Bool in "Java" `{ + return self.isPrintingKey(); + `} + + redef fun new_global_ref import sys, Sys.jni_env `{ + Sys sys = NativeKeyEvent_sys(self); + JNIEnv *env = Sys_jni_env(sys); + return (*env)->NewGlobalRef(env, self); + `} + + redef fun pop_from_local_frame_with_env(jni_env) `{ + return (*jni_env)->PopLocalFrame(jni_env, self); + `} +end + +# Java getter: android.view.KeyEvent.KEYCODE_BACK +fun android_view_key_event_keycode_back: Int in "Java" `{ + return android.view.KeyEvent.KEYCODE_BACK; +`} diff --git a/lib/android/landscape.nit b/lib/android/landscape.nit index be79eb5..c313efb 100644 --- a/lib/android/landscape.nit +++ b/lib/android/landscape.nit @@ -20,4 +20,4 @@ module landscape is android_manifest_activity """android:screenOrientation="sensorLandscape" """ end -import platform +import android diff --git a/lib/android/nit_activity.nit b/lib/android/nit_activity.nit index 87d9d22..1668147 100644 --- a/lib/android/nit_activity.nit +++ b/lib/android/nit_activity.nit @@ -43,6 +43,7 @@ end import platform import log import activities +import key_event import bundle import dalvik @@ -133,6 +134,36 @@ in "C body" `{ { Activity_on_restore_instance_state((Activity)nit_activity, saved_state); } + + JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnBackPressed + (JNIEnv *env, jobject java_activity, jint nit_activity) + { + return (jboolean)Activity_on_back_pressed((Activity)nit_activity); + } + + JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyDown + (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event) + { + return (jboolean)Activity_on_key_down((Activity)nit_activity, keyCode, event); + } + + JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyLongPress + (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event) + { + return (jboolean)Activity_on_key_long_press((Activity)nit_activity, keyCode, event); + } + + JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyMultiple + (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jint count, jobject event) + { + return (jboolean)Activity_on_key_multiple((Activity)nit_activity, keyCode, count, event); + } + + JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyUp + (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event) + { + return (jboolean)Activity_on_key_up((Activity)nit_activity, keyCode, event); + } `} # Wrapper to our Java `NitActivity` @@ -158,7 +189,10 @@ redef class App Activity.on_create, Activity.on_destroy, Activity.on_start, Activity.on_restart, Activity.on_stop, Activity.on_pause, Activity.on_resume, - Activity.on_save_instance_state, Activity.on_restore_instance_state `{ + Activity.on_save_instance_state, Activity.on_restore_instance_state, + Activity.on_back_pressed, + Activity.on_key_down, Activity.on_key_long_press, + Activity.on_key_multiple, Activity.on_key_up `{ App_incr_ref(self); global_app = self; `} @@ -251,6 +285,31 @@ class Activity # Notification from Android, the current device configuration has changed fun on_configuration_changed do end + + # The back key has been pressed + # + # Return `true` if the event has been handled. + fun on_back_pressed: Bool do return false + + # A key has been pressed + # + # Return `true` if the event has been handled. + fun on_key_down(key_code: Int, event: NativeKeyEvent): Bool do return false + + # A key has been long pressed + # + # Return `true` if the event has been handled. + fun on_key_long_press(key_code: Int, event: NativeKeyEvent): Bool do return false + + # Multiple down/up pairs of the same key have occurred in a row + # + # Return `true` if the event has been handled. + fun on_key_multiple(key_code, count: Int, event: NativeKeyEvent): Bool do return false + + # A key has been released + # + # Return `true` if the event has been handled. + fun on_key_up(key_code: Int, event: NativeKeyEvent): Bool do return false end # Set up global data in C and leave it to Android to callback Java, which we relay to Nit diff --git a/lib/android/portrait.nit b/lib/android/portrait.nit index e8f5720..0759c40 100644 --- a/lib/android/portrait.nit +++ b/lib/android/portrait.nit @@ -17,4 +17,4 @@ module portrait is android_manifest_activity """ android:screenOrientation="portrait" """ -import platform +import android diff --git a/lib/android/sensors.nit b/lib/android/sensors.nit index b969389..0681e4a 100644 --- a/lib/android/sensors.nit +++ b/lib/android/sensors.nit @@ -14,28 +14,30 @@ # 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" `{ diff --git a/lib/android/ui/ui.nit b/lib/android/ui/ui.nit index 65535e8..cd9f4ea 100644 --- a/lib/android/ui/ui.nit +++ b/lib/android/ui/ui.nit @@ -13,7 +13,10 @@ # limitations under the License. # Views and services to use the Android native user interface -module ui +module ui is + # `adjustPan` allows to use EditText in a ListLayout + android_manifest_activity """android:windowSoftInputMode="adjustPan"""" +end # Implementation note: # @@ -78,6 +81,19 @@ redef class App end end +redef class Activity + redef fun on_back_pressed + do + var window = app.window + if window.enable_back_button then + window.on_back_button + return true + end + + return false + end +end + # On Android, a window is implemented with the fragment `native` redef class Window redef var native = (new Android_app_Fragment(self)).new_global_ref @@ -228,7 +244,7 @@ redef class TextView else // if (align > 0.5d) g = android.view.Gravity.RIGHT; - view.setGravity(g); + view.setGravity(g | android.view.Gravity.CENTER_VERTICAL); `} end @@ -240,9 +256,26 @@ end redef class CheckBox redef type NATIVE: Android_widget_CompoundButton redef var native do return (new Android_widget_CheckBox(app.native_activity)).new_global_ref + init do set_callback_on_toggle(native) redef fun is_checked do return native.is_checked redef fun is_checked=(value) do native.set_checked(value) + + private fun on_toggle do notify_observers new ToggleEvent(self) + + private fun set_callback_on_toggle(view: NATIVE) + import on_toggle in "Java" `{ + final int final_sender_object = self; + CheckBox_incr_ref(final_sender_object); + + view.setOnCheckedChangeListener( + new android.widget.CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) { + CheckBox_on_toggle(final_sender_object); + } + }); + `} end redef class TextInput diff --git a/lib/app/audio.nit b/lib/app/audio.nit index 42d90ae..7f22c78 100644 --- a/lib/app/audio.nit +++ b/lib/app/audio.nit @@ -27,7 +27,6 @@ import app_base 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) diff --git a/lib/app/data_store.nit b/lib/app/data_store.nit index 4db4b22..ebebe83 100644 --- a/lib/app/data_store.nit +++ b/lib/app/data_store.nit @@ -23,7 +23,6 @@ import app_base 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) diff --git a/lib/app/ui.nit b/lib/app/ui.nit index 4efbb87..0afe7aa 100644 --- a/lib/app/ui.nit +++ b/lib/app/ui.nit @@ -18,9 +18,8 @@ module ui 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 @@ -28,8 +27,32 @@ redef class App # The current `Window` of this activity # - # This attribute must be set by refinements of `App`. - var window: Window is writable + # This attribute is set by `push_window`. + var window: Window is noinit + + # Make visible and push `window` on the top of `pop_window` + # + # This method must be called at least once within `App::on_create`. + # It can be called at any times while the app is active. + fun push_window(window: Window) + do + window_stack.add window + self.window = window + end + + # Pop the current `window` from the stack and show the previous one + # + # Require: `window_stack.not_empty` + fun pop_window + do + assert window_stack.not_empty + window_stack.pop + window = window_stack.last + window.on_resume + end + + # Stack of active windows + var window_stack = new Array[Window] redef fun on_create do window.on_create @@ -141,6 +164,12 @@ end # A window, root of the `Control` tree class Window super CompositeControl + + # Should the back button be shown and used to go back to a previous window? + fun enable_back_button: Bool do return app.window_stack.length > 1 + + # The back button has been pressed, usually to open the previous window + fun on_back_button do app.pop_window end # A viewable `Control` @@ -211,12 +240,29 @@ class CheckBox var is_checked = false is writable end +# Event sent from a `VIEW` +class ViewEvent + super AppEvent + + # The `VIEW` that raised this event + var sender: VIEW + + # Type of the `sender` + type VIEW: View +end + # A `Button` press event class ButtonPressEvent - super AppEvent + super ViewEvent + + redef type VIEW: Button +end + +# The `CheckBox` `sender` has been toggled +class ToggleEvent + super ViewEvent - # The `Button` that raised this event - var sender: Button + redef type VIEW: CheckBox end # A layout to visually organize `Control`s diff --git a/lib/bucketed_game.nit b/lib/bucketed_game.nit index b5542e4..28129a5 100644 --- a/lib/bucketed_game.nit +++ b/lib/bucketed_game.nit @@ -26,6 +26,7 @@ module bucketed_game is serialize import serialization +import counter # Something acting on the game abstract class Turnable[G: Game] @@ -65,6 +66,9 @@ class Buckets[G: Game] private var buckets: Array[BUCKET] = [for b in n_buckets.times do new HashSet[Bucketable[G]]] is lazy + # Stats on delays asked when adding an event with `act_in` and `act_next` + private var delays = new Counter[Int] + # Add the Bucketable event `e` at `at_tick`. fun add_at(e: Bucketable[G], at_tick: Int) do @@ -106,6 +110,26 @@ class Buckets[G: Game] end end end + + # Get some statistics on both the current held events and historic expired events + fun stats: String + do + var entries = 0 + var instances = new HashSet[Bucketable[G]] + var max = 0 + var min = 100000 + for bucket in buckets do + var len = bucket.length + entries += len + instances.add_all bucket + min = min.min(len) + max = max.max(len) + end + var avg = entries.to_f / buckets.length.to_f + + return "{buckets.length} buckets; uniq/tot:{instances.length}/{entries}, avg:{avg.to_precision(1)}, min:{min}, max:{max}\n" + + "history:{delays.sum}, avg:{delays.avg}, min:{delays[delays.min.as(not null)]}, max:{delays[delays.max.as(not null)]}" + end end # Game related event @@ -123,16 +147,16 @@ end # Game logic on the client class ThinGame - # Game tick when `self` should act. + # Current game tick # # Default is 0. - var tick: Int = 0 is protected writable + var tick: Int = 0 is writable end # Game turn on the client class ThinGameTurn[G: ThinGame] - # Game tick when `self` should act. + # Game tick when happened this turn var tick: Int is protected writable # Game events occurred for `self`. @@ -153,10 +177,18 @@ class GameTurn[G: Game] end # Insert the Bucketable event `e` to be executed at next tick. - fun act_next(e: Bucketable[G]) do game.buckets.add_at(e, tick + 1) + fun act_next(e: Bucketable[G]) + do + game.buckets.add_at(e, tick + 1) + game.buckets.delays.inc(1) + end # Insert the Bucketable event `e` to be executed at tick `t`. - fun act_in(e: Bucketable[G], t: Int) do game.buckets.add_at(e, tick + t) + fun act_in(e: Bucketable[G], t: Int) + do + game.buckets.add_at(e, tick + t) + game.buckets.delays.inc(t) + end # Add and `apply` a game `event`. fun add_event( event : GameEvent ) diff --git a/lib/core/text/abstract_text.nit b/lib/core/text/abstract_text.nit index c9d1279..d7dfa48 100644 --- a/lib/core/text/abstract_text.nit +++ b/lib/core/text/abstract_text.nit @@ -2160,6 +2160,11 @@ redef class NativeString # 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] diff --git a/lib/core/text/flat.nit b/lib/core/text/flat.nit index b7e8e61..9e70321 100644 --- a/lib/core/text/flat.nit +++ b/lib/core/text/flat.nit @@ -369,6 +369,10 @@ redef class FlatText 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. @@ -666,15 +670,15 @@ private class ASCIIFlatString end redef fun substring(from, count) do + var ln = _length + if count <= 0 then return "" + if (count + from) > ln then count = ln - from if count <= 0 then return "" - if from < 0 then count += from - if count < 0 then return "" + if count <= 0 then return "" from = 0 end - var ln = _length - if (count + from) > ln then count = ln - from return new ASCIIFlatString.full_data(_items, count, from + _first_byte, count) end diff --git a/lib/core/text/ropes.nit b/lib/core/text/ropes.nit index f370e49..0bf97b8 100644 --- a/lib/core/text/ropes.nit +++ b/lib/core/text/ropes.nit @@ -221,23 +221,16 @@ private class Concat 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` diff --git a/lib/gamnit/cameras.nit b/lib/gamnit/cameras.nit index a1eed2f..245d071 100644 --- a/lib/gamnit/cameras.nit +++ b/lib/gamnit/cameras.nit @@ -162,6 +162,35 @@ class EulerCamera yaw = 0.0 roll = 0.0 end + + # Convert the position `x, y` on screen, to world coordinates on the plane at `target_z` + # + # `target_z` defaults to `0.0` and specifies the Z coordinates of the plane + # on which to project the screen position `x, y`. + # + # This method assumes that the camera is looking along the Z axis towards higher values. + # Using it in a different orientation can be useful, but won't result in valid + # world coordinates. + fun camera_to_world(x, y: Numeric, target_z: nullable Float): Point[Float] + do + # TODO, this method could be tweaked to support projecting the 2D point, + # on the near plane (x,y) onto a given distance no matter to orientation + # of the camera. + + target_z = target_z or else 0.0 + + # Convert from pixel units / window resolution to + # units on the near clipping wall to + # units on the target wall at Z = 0 + var near_height = (field_of_view_y/2.0).tan * near + var cross_screen_to_near = near_height / (display.height.to_f/2.0) + var cross_near_to_target = (position.z - target_z) / near + var mod = cross_screen_to_near * cross_near_to_target * 1.72 # FIXME drop the magic number + + var wx = position.x + (x.to_f-display.width.to_f/2.0) * mod + var wy = position.y - (y.to_f-display.height.to_f/2.0) * mod + return new Point[Float](wx, wy) + end end # Orthogonal camera to draw UI objects with services to work with screens of different sizes diff --git a/lib/gamnit/depth/depth_core.nit b/lib/gamnit/depth/depth_core.nit index 03a3970..f1e5803 100644 --- a/lib/gamnit/depth/depth_core.nit +++ b/lib/gamnit/depth/depth_core.nit @@ -103,6 +103,9 @@ class Mesh # Coordinates on the texture per vertex var texture_coords = new Array[Float] is lazy, writable + # `GLDrawMode` used to display this mesh, defaults to `gl_TRIANGLES` + fun draw_mode: GLDrawMode do return gl_TRIANGLES + # Create an UV sphere of `radius` with `n_meridians` and `n_parallels` init uv_sphere(radius: Float, n_meridians, n_parallels: Int) do diff --git a/lib/gamnit/depth/more_materials.nit b/lib/gamnit/depth/more_materials.nit index 613ff1a..b77c85e 100644 --- a/lib/gamnit/depth/more_materials.nit +++ b/lib/gamnit/depth/more_materials.nit @@ -75,9 +75,9 @@ class SmoothMaterial # Execute draw if mesh.indices.is_empty then - glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3) + glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3) else - glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array) + glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array) end end end @@ -154,11 +154,13 @@ class TexturedMaterial var xd = sample_used_texture.offset_right - xa var ya = sample_used_texture.offset_top var yd = sample_used_texture.offset_bottom - ya + xd *= 0.999 + yd *= 0.999 var tex_coords = new Array[Float].with_capacity(mesh.texture_coords.length) for i in [0..mesh.texture_coords.length/2[ do tex_coords[i*2] = xa + xd * mesh.texture_coords[i*2] - tex_coords[i*2+1] = ya + yd * mesh.texture_coords[i*2+1] + tex_coords[i*2+1] = 1.0 - (ya + yd * mesh.texture_coords[i*2+1]) end program.tex_coord.array(tex_coords, 2) @@ -180,9 +182,9 @@ class TexturedMaterial program.camera.uniform(app.world_camera.position.x, app.world_camera.position.y, app.world_camera.position.z) if mesh.indices.is_empty then - glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3) + glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3) else - glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array) + glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array) end end end @@ -218,9 +220,9 @@ class NormalsMaterial program.normal.array(mesh.normals, 3) if mesh.indices.is_empty then - glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3) + glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3) else - glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array) + glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array) end end end diff --git a/lib/gamnit/depth/more_meshes.nit b/lib/gamnit/depth/more_meshes.nit index 9f2318a..569973b 100644 --- a/lib/gamnit/depth/more_meshes.nit +++ b/lib/gamnit/depth/more_meshes.nit @@ -124,7 +124,8 @@ class Cube var d = [1.0, 0.0] var texture_coords = new Array[Float] - for v in [c, d, a, a, d, b] do for i in 6.times do texture_coords.add_all v + var face = [a, c, d, a, d, b] + for i in 6.times do for v in face do texture_coords.add_all v return texture_coords end diff --git a/lib/gamnit/depth/more_models.nit b/lib/gamnit/depth/more_models.nit index a126223..c2b9664 100644 --- a/lib/gamnit/depth/more_models.nit +++ b/lib/gamnit/depth/more_models.nit @@ -50,7 +50,7 @@ class ModelAsset if leaves.is_empty then # Nothing was loaded, use a cube with the default material - var leaf = new LeafModel(new Cube, new SmoothMaterial.default) + var leaf = placeholder_model leaves.add leaf end end @@ -168,7 +168,7 @@ private class ModelFromObj # Load textures need for these materials for name in texture_names do if not asset_textures_by_name.keys.has(name) then - var tex = new GamnitAssetTexture(name) + var tex = new TextureAsset(name) asset_textures_by_name[name] = tex end end @@ -379,8 +379,13 @@ end redef class Sys # Textures loaded from .mtl files for models - var asset_textures_by_name = new Map[String, GamnitAssetTexture] + var asset_textures_by_name = new Map[String, TextureAsset] # All instantiated asset models var models = new Set[ModelAsset] + + # Blue cube of 1 unit on each side, acting as placeholder for models failing to load + # + # This model can be freely used by any `Actor` as placeholder or for debugging. + var placeholder_model = new LeafModel(new Cube, new SmoothMaterial.default) is lazy end diff --git a/lib/gamnit/display.nit b/lib/gamnit/display.nit index 69ddb41..6e7c58c 100644 --- a/lib/gamnit/display.nit +++ b/lib/gamnit/display.nit @@ -44,6 +44,15 @@ class GamnitDisplay # Only affects the desktop implementations. var show_cursor: Bool = true is writable + # Number of bits used for the red value in the color buffer + fun red_bits: Int do return 8 + + # Number of bits used for the green value in the color buffer + fun green_bits: Int do return 8 + + # Number of bits used for the blue value in the color buffer + fun blue_bits: Int do return 8 + # Prepare this display # # The implementation varies per platform. diff --git a/lib/gamnit/display_android.nit b/lib/gamnit/display_android.nit index 3997c4a..e9d1b4a 100644 --- a/lib/gamnit/display_android.nit +++ b/lib/gamnit/display_android.nit @@ -21,7 +21,7 @@ module display_android is android_manifest """""" end -import ::android +import ::android::game intrude import android::load_image private import gamnit::egl @@ -37,7 +37,7 @@ redef class GamnitDisplay setup_egl_display native_display # We need 8 bits per color for selection by color - select_egl_config(8, 8, 8, 0, 8, 0, 0) + select_egl_config(red_bits, green_bits, blue_bits, 0, 8, 0, 0) var format = egl_config.attribs(egl_display).native_visual_id native_window.set_buffers_geometry(0, 0, format) @@ -48,7 +48,7 @@ redef class GamnitDisplay redef fun close do close_egl end -redef class GamnitAssetTexture +redef class TextureAsset redef fun load_from_platform do diff --git a/lib/gamnit/display_linux.nit b/lib/gamnit/display_linux.nit index 8f115e7..1dbee21 100644 --- a/lib/gamnit/display_linux.nit +++ b/lib/gamnit/display_linux.nit @@ -48,7 +48,7 @@ redef class GamnitDisplay setup_egl_display x11_display if debug_gamnit then print "Setting up EGL context" - select_egl_config(8, 8, 8, 8, 8, 0, 0) + select_egl_config(red_bits, green_bits, blue_bits, 8, 8, 0, 0) setup_egl_context window_handle end @@ -95,7 +95,7 @@ redef class GamnitDisplay end end -redef class GamnitAssetTexture +redef class TextureAsset redef fun load_from_platform do diff --git a/lib/gamnit/egl.nit b/lib/gamnit/egl.nit index 3fcb8c8..b451362 100644 --- a/lib/gamnit/egl.nit +++ b/lib/gamnit/egl.nit @@ -46,14 +46,14 @@ redef class GamnitDisplay end # Select an EGL config - protected fun select_egl_config(blue, green, red, alpha, depth, stencil, sample: Int) + protected fun select_egl_config(red, green, blue, alpha, depth, stencil, sample: Int) do var config_chooser = new EGLConfigChooser config_chooser.renderable_type_egl config_chooser.surface_type_egl - config_chooser.blue_size = blue - config_chooser.green_size = green config_chooser.red_size = red + config_chooser.green_size = green + config_chooser.blue_size = blue if alpha > 0 then config_chooser.alpha_size = alpha if depth > 0 then config_chooser.depth_size = depth if stencil > 0 then config_chooser.stencil_size = stencil diff --git a/lib/gamnit/limit_fps.nit b/lib/gamnit/limit_fps.nit index a300b54..896f07f 100644 --- a/lib/gamnit/limit_fps.nit +++ b/lib/gamnit/limit_fps.nit @@ -29,8 +29,8 @@ redef class App # Current frame-rate # - # Updated each 5 seconds. - var current_fps = 0.0 + # Updated each 5 seconds, initialized at the value of `maximum_fps`. + var current_fps: Float = maximum_fps is lazy redef fun frame_full do @@ -47,7 +47,7 @@ redef class App private var frame_count = 0 # Deadline used to compute `current_fps` - private var frame_count_deadline = 0 + private var frame_count_deadline = 5.0 # Check and sleep to maintain a frame-rate bellow `maximum_fps` # @@ -55,12 +55,12 @@ redef class App # 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 diff --git a/lib/gamnit/programs.nit b/lib/gamnit/programs.nit index 1a995ef..6859c76 100644 --- a/lib/gamnit/programs.nit +++ b/lib/gamnit/programs.nit @@ -428,6 +428,23 @@ abstract class GamnitProgram end end + # Diagnose possible problems with the shaders of the program + # + # Lists to the console inactive uniforms and attributes. + # These may not be problematic but they can help to debug the program. + fun diagnose + do + if gl_program == null then compile_and_link + + print "# Diagnose {class_name}" + for k,v in uniforms do + if not v.is_active then print "* Uniform {v.name} is inactive" + end + for k,v in attributes do + if not v.is_active then print "* Attribute {v.name} is inactive" + end + end + # Attributes of this program organized by name # # Active attributes are gathered at `compile_and_link`. diff --git a/contrib/asteronits/src/texture_atlas_parser.nit b/lib/gamnit/texture_atlas_parser.nit similarity index 98% rename from contrib/asteronits/src/texture_atlas_parser.nit rename to lib/gamnit/texture_atlas_parser.nit index 113b887..a0e4d5f 100644 --- a/contrib/asteronits/src/texture_atlas_parser.nit +++ b/lib/gamnit/texture_atlas_parser.nit @@ -44,7 +44,7 @@ var attributes = new Array[String] # Insert the first attribute, to load the root texture var png_file = "images" / xml_file.basename("xml") + "png" attributes.add """ - var root_texture = new Texture("{{{png_file}}}")""" + var root_texture = new TextureAsset("{{{png_file}}}")""" # Read XML file var content = xml_file.to_path.read_all diff --git a/lib/gamnit/textures.nit b/lib/gamnit/textures.nit index 415aa90..d644d60 100644 --- a/lib/gamnit/textures.nit +++ b/lib/gamnit/textures.nit @@ -21,7 +21,7 @@ import display abstract class Texture # Prepare a texture located at `path` within the `assets` folder - new (path: Text) do return new GamnitAssetTexture(path.to_s) + new (path: Text) do return new TextureAsset(path.to_s) # Root texture of which `self` is derived fun root: GamnitRootTexture is abstract @@ -41,7 +41,7 @@ abstract class Texture # OpenGL handle to this texture fun gl_texture: Int do return root.gl_texture - # Prepare a subtexture from this texture + # Prepare a subtexture from this texture, from the given pixel offsets fun subtexture(left, top, width, height: Numeric): GamnitSubtexture do # Setup the subtexture @@ -143,7 +143,7 @@ class GamnitRootTexture end # Texture loaded from the assets folder -class GamnitAssetTexture +class TextureAsset super GamnitRootTexture # Path to this texture within the `assets` folder diff --git a/lib/geometry/polygon.nit b/lib/geometry/polygon.nit index 94ad1c9..1be832e 100644 --- a/lib/geometry/polygon.nit +++ b/lib/geometry/polygon.nit @@ -441,31 +441,40 @@ fun turn_left(p1, p2, p3: Point[Float]): Bool do end # Split a polygon into triangles -# Useful for converting a concave polygon into multiple convex ones -fun triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do - var poly = new Polygon(pts) - pts = poly.points - recursive_triangulate(pts, results) +# +# Useful for converting a concave polygon into multiple convex ones. +# +# See: the alternative `triangulate_recursive` uses arrays in-place. +fun triangulate(points: Array[Point[Float]]): Array[ConvexPolygon] +do + var results = new Array[ConvexPolygon] + triangulate_recursive(points.clone, results) + return results end -private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do - if pts.length == 3 then - results.add(new ConvexPolygon(pts)) +# Split a polygon into triangles using arrays in-place +# +# Consumes the content of `points` and add the triangles to `results`. +# +# See: the alternative `triangulate` which does not modify `points` and returns a new array. +fun triangulate_recursive(points: Array[Point[Float]], results: Array[ConvexPolygon]) do + if points.length == 3 then + results.add(new ConvexPolygon(points)) return end - var prev = pts[pts.length - 1] - var curr = pts[0] - var next = pts[1] - for i in [1..pts.length[ do + var prev = points[points.length - 1] + var curr = points[0] + var next = points[1] + for i in [1..points.length[ do if turn_left(prev, curr, next) then - prev = pts[i-1] + prev = points[i-1] curr = next - if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1] + if i+1 == points.length then next = points[points.length - 1] else next = points[i+1] continue end var contains = false var triangle = new ConvexPolygon(new Array[Point[Float]].with_items(prev, curr, next)) - for p in pts do + for p in points do if p != prev and p != curr and p != next then if triangle.contain(p) then contains = true @@ -475,12 +484,12 @@ private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[Conve end if not contains then results.add(triangle) - pts.remove(curr) - recursive_triangulate(pts, results) + points.remove(curr) + triangulate_recursive(points, results) break end - prev = pts[i-1] + prev = points[i-1] curr = next - if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1] + if i+1 == points.length then next = points[points.length - 1] else next = points[i+1] end end diff --git a/lib/ios/ui/ui.nit b/lib/ios/ui/ui.nit index a4a0000..48c7e12 100644 --- a/lib/ios/ui/ui.nit +++ b/lib/ios/ui/ui.nit @@ -25,16 +25,16 @@ in "ObjC" `{ @interface NitCallbackReference: NSObject // Nit object target of the callbacks from UI events - @property (nonatomic) Button nit_button; + @property (nonatomic) View nit_view; // Actual callback method - -(void) nitOnEvent: (UIButton*) sender; + -(void) nitOnEvent: (UIView*) sender; @end @implementation NitCallbackReference - -(void) nitOnEvent: (UIButton*) sender { - Button_on_click(self.nit_button); + -(void) nitOnEvent: (UIView*) sender { + View_on_ios_event(self.nit_view); } @end @@ -112,6 +112,19 @@ redef class App set_view_controller(app_delegate.window, window.native) super end + + # Use iOS ` popViewControllerAnimated` + redef fun pop_window + do + window_stack.pop + pop_view_controller app_delegate.window + window.on_resume + end + + private fun pop_view_controller(window: UIWindow) in "ObjC" `{ + UINavigationController *navController = (UINavigationController*)window.rootViewController; + [navController popViewControllerAnimated: YES]; + `} end redef class AppDelegate @@ -136,6 +149,8 @@ redef class View redef type NATIVE: UIView redef var enabled = null is lazy + + private fun on_ios_event do end end redef class CompositeControl @@ -180,7 +195,6 @@ redef class Layout init do native.alignment = new UIStackViewAlignment.fill - native.distribution = new UIStackViewDistribution.fill_equally # TODO make customizable native.spacing = 4.0 @@ -197,11 +211,19 @@ redef class Layout end redef class HorizontalLayout - redef init do native.axis = new UILayoutConstraintAxis.horizontal + redef init + do + native.axis = new UILayoutConstraintAxis.horizontal + native.distribution = new UIStackViewDistribution.fill_equally + end end redef class VerticalLayout - redef init do native.axis = new UILayoutConstraintAxis.vertical + redef init + do + native.axis = new UILayoutConstraintAxis.vertical + native.distribution = new UIStackViewDistribution.equal_spacing + end end redef class Label @@ -253,15 +275,20 @@ redef class CheckBox # `UISwitch` acting as the real check box var ui_switch: UISwitch is noautoinit - init do + redef fun on_ios_event do notify_observers new ToggleEvent(self) + + init + do # Tweak the layout so it is centered - layout.native.distribution = new UIStackViewDistribution.fill_proportionally - layout.native.alignment = new UIStackViewAlignment.center + layout.native.distribution = new UIStackViewDistribution.equal_spacing + layout.native.alignment = new UIStackViewAlignment.fill layout.native.layout_margins_relative_arrangement = true var s = new UISwitch native.add_arranged_subview s ui_switch = s + + ui_switch.set_callback self end redef fun text=(text) do lbl.text = text @@ -271,6 +298,23 @@ redef class CheckBox redef fun is_checked=(value) do ui_switch.set_on_animated(value, true) end +redef class UISwitch + # Register callbacks on this switch to be relayed to `sender` + private fun set_callback(sender: View) + import View.on_ios_event in "ObjC" `{ + + NitCallbackReference *ncr = [[NitCallbackReference alloc] init]; + ncr.nit_view = sender; + + // Pin the objects in both Objective-C and Nit GC + View_incr_ref(sender); + ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr); + + [self addTarget:ncr action:@selector(nitOnEvent:) + forControlEvents:UIControlEventValueChanged]; + `} +end + redef class TextInput redef type NATIVE: UITextField @@ -293,25 +337,25 @@ redef class Button init do native.set_callback self + redef fun on_ios_event do notify_observers new ButtonPressEvent(self) + redef fun text=(text) do if text != null then native.title = text.to_nsstring redef fun text do return native.current_title.to_s - private fun on_click do notify_observers new ButtonPressEvent(self) - redef fun enabled=(enabled) do native.enabled = enabled or else true redef fun enabled do return native.enabled end redef class UIButton # Register callbacks on this button to be relayed to `sender` - private fun set_callback(sender: Button) - import Button.on_click in "ObjC" `{ + private fun set_callback(sender: View) + import View.on_ios_event in "ObjC" `{ NitCallbackReference *ncr = [[NitCallbackReference alloc] init]; - ncr.nit_button = sender; + ncr.nit_view = sender; // Pin the objects in both Objective-C and Nit GC - Button_incr_ref(sender); + View_incr_ref(sender); ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr); [self addTarget:ncr action:@selector(nitOnEvent:) @@ -333,7 +377,7 @@ redef class ListLayout native_stack_view.translates_autoresizing_mask_into_constraits = false native_stack_view.axis = new UILayoutConstraintAxis.vertical native_stack_view.alignment = new UIStackViewAlignment.fill - native_stack_view.distribution = new UIStackViewDistribution.fill_equally + native_stack_view.distribution = new UIStackViewDistribution.equal_spacing native_stack_view.spacing = 4.0 native.add_subview native_stack_view diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index 4d00782..45578f2 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -16,22 +16,22 @@ # 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 # @@ -49,16 +49,28 @@ # 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. @@ -81,6 +93,10 @@ # 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 @@ -90,7 +106,8 @@ module serialization 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 @@ -103,14 +120,14 @@ 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: # @@ -119,7 +136,7 @@ class JsonSerializer # * 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 @@ -162,7 +179,7 @@ class JsonSerializer end first_attribute = true - object.serialize_to_json self + object.accept_json_serializer self first_attribute = false if plain_json then open_objects.pop @@ -220,10 +237,10 @@ class JsonDeserializer 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. # @@ -232,7 +249,7 @@ class JsonDeserializer 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 @@ -268,7 +285,7 @@ class JsonDeserializer 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"] @@ -467,7 +484,7 @@ class JsonDeserializer # 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 @@ -492,11 +509,44 @@ redef class Text 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 "\{" @@ -515,59 +565,35 @@ redef class Serializable 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] @@ -595,7 +621,7 @@ 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 @@ -638,7 +664,7 @@ redef class SimpleCollection[E] 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) @@ -654,7 +680,7 @@ redef class Map[K, V] 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 diff --git a/lib/json/static.nit b/lib/json/static.nit index 565da1f..e333b85 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -31,6 +31,9 @@ private import json_lexer 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 diff --git a/lib/linux/ui.nit b/lib/linux/ui.nit index ca004a0..1e26f17 100644 --- a/lib/linux/ui.nit +++ b/lib/linux/ui.nit @@ -43,7 +43,7 @@ redef class App bar.title = "app.nit" # TODO offer a portable API to name windows bar.show_close_button = true - # TODO add back button + bar.add back_button.native return bar end @@ -55,6 +55,9 @@ redef class App return stack end + # Button on the header bar to go back + var back_button = new BackButton is lazy + # On GNU/Linux, we go through all the callbacks once, # there is no complex life-cycle. redef fun run @@ -64,7 +67,6 @@ redef class App app.on_start app.on_resume - native_window.show_all gtk_main app.on_pause @@ -88,7 +90,13 @@ redef class App # improved with GTK 3.18 and interpolate_size. native_window.resizable = false + native_window.show_all + super + + if window.enable_back_button then + back_button.native.show + else back_button.native.hide end end @@ -243,6 +251,21 @@ redef class Button init do native.signal_connect("clicked", self, null) end +# Button to go back between windows +class BackButton + super Button + + # TODO i18n + redef fun text=(value) do super(value or else "Back") + + redef fun signal(sender, data) + do + super + + app.window.on_back_button + end +end + redef class Label redef type NATIVE: GtkLabel redef var native = new GtkLabel("") @@ -294,6 +317,9 @@ redef class CheckBox redef type NATIVE: GtkCheckButton redef var native = new GtkCheckButton + redef fun signal(sender, data) do notify_observers new ToggleEvent(self) + init do native.signal_connect("toggled", self, null) + redef fun text do return native.text redef fun text=(value) do native.text = (value or else "").to_s diff --git a/lib/mnit/android/android_app.nit b/lib/mnit/android/android_app.nit index c8f02e3..e78da16 100644 --- a/lib/mnit/android/android_app.nit +++ b/lib/mnit/android/android_app.nit @@ -22,7 +22,7 @@ module android_app is android_manifest_activity """ import mnit import mnit::opengles1 -import ::android +import ::android::game intrude import ::android::input_events in "C" `{ diff --git a/lib/mnit/mnit_fps.nit b/lib/mnit/mnit_fps.nit index 3841bc6..f4abd92 100644 --- a/lib/mnit/mnit_fps.nit +++ b/lib/mnit/mnit_fps.nit @@ -44,19 +44,19 @@ redef class App private var frame_count = 0 # Deadline used to compute `current_fps` - private var frame_count_deadline = 0 + private var frame_count_deadline = 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 diff --git a/lib/nitcorn/README.md b/lib/nitcorn/README.md index 6bc8b36..2db66b8 100644 --- a/lib/nitcorn/README.md +++ b/lib/nitcorn/README.md @@ -26,7 +26,8 @@ and the FFI enables calling services in different languages. A minimal example follows with a custom `Action` and using `FileServer`. More general examples are available at `lib/nitcorn/examples/`. -It includes the configuration of `http://xymus.net/` which merges many other _nitcorn_ applications. +For an example of a larger project merging many _nitcorn_ applications into one server, +take a look at the configuration of `http://xymus.net/` at `../contrib/xymus_net/xymus_net.nit`. Larger projects using _nitcorn_ can be found in the `contrib/` folder: * _opportunity_ is a meetup planner heavily based on _nitcorn_. diff --git a/lib/nitcorn/examples/Makefile b/lib/nitcorn/examples/Makefile index d1bdeae..b1bbed9 100644 --- a/lib/nitcorn/examples/Makefile +++ b/lib/nitcorn/examples/Makefile @@ -2,10 +2,6 @@ all: bin/restful_annot mkdir -p bin/ ../../../bin/nitc --dir bin src/nitcorn_hello_world.nit src/simple_file_server.nit -xymus.net: - mkdir -p bin/ - ../../../bin/nitc --dir bin/ src/xymus_net.nit - pre-build: src/restful_annot_gen.nit src/restful_annot_gen.nit: ../../../bin/nitrestful -o $@ src/restful_annot.nit diff --git a/lib/nitcorn/file_server.nit b/lib/nitcorn/file_server.nit index ea3212d..5995558 100644 --- a/lib/nitcorn/file_server.nit +++ b/lib/nitcorn/file_server.nit @@ -71,6 +71,9 @@ class FileServer # Caching attributes of served files, used as the `cache-control` field in response headers var cache_control = "public, max-age=360" is writable + # Show directory listing? + var show_directory_listing = true is writable + redef fun answer(request, turi) do var response @@ -105,8 +108,8 @@ class FileServer end end - response = new HttpResponse(200) - if local_file.file_stat.is_dir then + var is_dir = local_file.file_stat.is_dir + if show_directory_listing and is_dir then # Show the directory listing var title = turi var files = local_file.files @@ -131,6 +134,7 @@ class FileServer header_code = header.write_to_string else header_code = "" + response = new HttpResponse(200) response.body = """ @@ -154,8 +158,9 @@ class FileServer """ response.header["Content-Type"] = media_types["html"].as(not null) - else + else if not is_dir then # It's a single file + response = new HttpResponse(200) response.files.add local_file var ext = local_file.file_extension @@ -168,8 +173,8 @@ class FileServer # Cache control response.header["cache-control"] = cache_control - end + else response = new HttpResponse(404) else response = new HttpResponse(404) else response = new HttpResponse(403) diff --git a/lib/nitcorn/media_types.nit b/lib/nitcorn/media_types.nit index 73c9626..37dd9db 100644 --- a/lib/nitcorn/media_types.nit +++ b/lib/nitcorn/media_types.nit @@ -51,6 +51,7 @@ class MediaTypes types["jar"] = "application/java-archive" types["war"] = "application/java-archive" types["ear"] = "application/java-archive" + types["json"] = "application/json" types["hqx"] = "application/mac-binhex40" types["pdf"] = "application/pdf" types["cco"] = "application/x-cocoa" diff --git a/lib/nitcorn/restful.nit b/lib/nitcorn/restful.nit index bccfd2c..158643a 100644 --- a/lib/nitcorn/restful.nit +++ b/lib/nitcorn/restful.nit @@ -35,11 +35,13 @@ class RestfulAction 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 diff --git a/lib/nitcorn/sessions.nit b/lib/nitcorn/sessions.nit index 4d54aa9..005a4dd 100644 --- a/lib/nitcorn/sessions.nit +++ b/lib/nitcorn/sessions.nit @@ -72,7 +72,7 @@ end redef class HttpRequest # The `Session` associated to this request - var session: nullable Session = null + var session: nullable Session = null is writable end redef class HttpRequestParser diff --git a/lib/nitcorn/vararg_routes.nit b/lib/nitcorn/vararg_routes.nit index 07664f6..3ec5dc1 100644 --- a/lib/nitcorn/vararg_routes.nit +++ b/lib/nitcorn/vararg_routes.nit @@ -226,6 +226,8 @@ private class UriParam # Parameters match everything. redef fun match(part) do return true + + redef fun to_s do return name end # A static uri string like `users`. @@ -237,6 +239,8 @@ private class UriString # Empty strings match everything otherwise matching is based on string equality. redef fun match(part) do return string.is_empty or string == part + + redef fun to_s do return string end redef class Routes diff --git a/lib/noise.nit b/lib/noise.nit index ba09bc1..cf4d0fa 100644 --- a/lib/noise.nit +++ b/lib/noise.nit @@ -359,3 +359,72 @@ redef universal Int return self & 0x3FFF_FFFF end end + +redef universal Float + # Smoothened `self`, used by `ImprovedNoise` + private fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0) +end + +# Direct translation of Ken Perlin's improved noise Java implementation +# +# This implementation differs from `PerlinNoise` on two main points. +# This noise is calculated for a 3D point, vs 2D in `PerlinNoise`. +# `PerlinNoise` is based off a customizable seed, while this noise has a static data source. +class ImprovedNoise + + # Permutations + private var p: Array[Int] = [151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180] * 2 + + # Noise value in [-1..1] at 3D coordinates `x, y, z` + fun noise(x, y, z: Float): Float + do + var xx = x.floor.to_i & 255 + var yy = y.floor.to_i & 255 + var zz = z.floor.to_i & 255 + + x -= x.floor + y -= y.floor + z -= z.floor + + var u = x.fade + var v = y.fade + var w = z.fade + + var a = p[xx ] + yy + var aa = p[a ] + zz + var ab = p[a+1 ] + zz + var b = p[xx+1] + yy + var ba = p[b ] + zz + var bb = p[b+1 ] + zz + + return w.lerp(v.lerp(u.lerp(grad(p[aa ], x, y, z ), + grad(p[ba ], x-1.0, y, z )), + u.lerp(grad(p[ab ], x, y-1.0, z ), + grad(p[bb ], x-1.0, y-1.0, z ))), + v.lerp(u.lerp(grad(p[aa+1], x, y, z-1.0), + grad(p[ba+1], x-1.0, y, z-1.0)), + u.lerp(grad(p[ab+1], x, y-1.0, z-1.0), + grad(p[bb+1], x-1.0, y-1.0, z-1.0)))) + end + + # Value at a corner of the grid + private fun grad(hash: Int, x, y, z: Float): Float + do + var h = hash & 15 + var u = if h < 8 then x else y + var v = if h < 4 then y else if h == 12 or h == 14 then x else z + return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v) + end +end diff --git a/lib/performance_analysis.nit b/lib/performance_analysis.nit index 4763061..b9e436a 100644 --- a/lib/performance_analysis.nit +++ b/lib/performance_analysis.nit @@ -122,11 +122,8 @@ class PerfEntry # 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 diff --git a/lib/popcorn/.gitignore b/lib/popcorn/.gitignore new file mode 100644 index 0000000..c32211a --- /dev/null +++ b/lib/popcorn/.gitignore @@ -0,0 +1 @@ +tests/out diff --git a/lib/popcorn/Makefile b/lib/popcorn/Makefile new file mode 100644 index 0000000..12db3df --- /dev/null +++ b/lib/popcorn/Makefile @@ -0,0 +1,24 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +NITUNIT=../../bin/nitunit + +check: + $(NITUNIT) README.md + $(NITUNIT) pop_routes.nit + $(NITUNIT) pop_handlers.nit + $(NITUNIT) popcorn.nit + cd tests; make check diff --git a/lib/popcorn/README.md b/lib/popcorn/README.md new file mode 100644 index 0000000..4712faf --- /dev/null +++ b/lib/popcorn/README.md @@ -0,0 +1,815 @@ +# Popcorn + +**Why endure plain corn when you can pop it?!** + +Popcorn is a minimal yet powerful nit web application framework that provides cool +features for lazy developpers. + +Popcorn is built over nitcorn to provide a clean and user friendly interface +without all the boiler plate code. + +## What does it taste like? + +Set up is quick and easy as 10 lines of code. +Create a file `app.nit` and add the following code: + +~~~ +import popcorn + +class HelloHandler + super Handler + + redef fun get(req, res) do res.html "

Hello World!

" +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 +

Hello World!

+ +$ curl localhost:3000/wrong_uri + + + + +Not Found + + +

404 Not Found

+ + +~~~ + +This is why we love popcorn! + +## Basic routing + +**Routing** refers to determining how an application responds to a client request +to a particular endpoint, which is a URI (or path) and a specific HTTP request +method GET, POST, PUT or DELETE (other methods are not suported yet). + +Each route can have one or more handler methods, which are executed when the route is matched. + +Route handlers definition takes the following form: + +~~~nitish +import popcorn + +class MyHandler + super Handler + + redef fun METHOD(req, res) do end +end +~~~ + +Where: +* `MyHandler` is the name of the handler you will add to the app. +* `METHOD` can be replaced by `get`, `post`, `put` or `delete`. + +The following example responds to GET and POST requests: + +~~~ +import popcorn + +class MyHandler + super Handler + + redef fun get(req, res) do res.send "Got a GET request" + redef fun post(req, res) do res.send "Got a POST request" +end +~~~ + +To make your handler responds to a specific route, you have to add it to the app. + +Respond to POST request on the root route (`/`), the application's home page: + +~~~ +var app = new App +app.use("/", new MyHandler) +~~~ + +Respond to a request to the `/user` route: + +~~~ +app.use("/user", new MyHandler) +~~~ + +For more details about routing, see the routing section. + +## Serving static files with Popcorn + +To serve static files such as images, CSS files, and JavaScript files, use the +Popcorn built-in handler `StaticHandler`. + +Pass the name of the directory that contains the static assets to the StaticHandler +init method to start serving the files directly. +For example, use the following code to serve images, CSS files, and JavaScript files +in a directory named `public`: + +~~~ +app.use("/", new StaticHandler("public/")) +~~~ + +Now, you can load the files that are in the `public` directory: + +~~~raw +http://localhost:3000/images/trollface.jpg +http://localhost:3000/css/style.css +http://localhost:3000/js/app.js +http://localhost:3000/hello.html +~~~ + +Popcorn looks up the files relative to the static directory, so the name of the +static directory is not part of the URL. +To use multiple static assets directories, add the `StaticHandler` multiple times: + +~~~ +app.use("/", new StaticHandler("public/")) +app.use("/", new StaticHandler("files/")) +~~~ + +Popcorn looks up the files in the order in which you set the static directories +with the `use` method. + +To create a virtual path prefix (where the path does not actually exist in the file system) +for files that are served by the `StaticHandler`, specify a mount path for the +static directory, as shown below: + +~~~ +app.use("/static/", new StaticHandler("public/")) +~~~ + +Now, you can load the files that are in the public directory from the `/static` +path prefix. + +~~~raw +http://localhost:3000/static/images/trollface.jpg +http://localhost:3000/static/css/style.css +http://localhost:3000/static/js/app.js +http://localhost:3000/static/hello.html +~~~ + +However, the path that you provide to the `StaticHandler` is relative to the +directory from where you launch your app. +If you run the app from another directory, it’s safer to use the absolute path of +the directory that you want to serve. + +## Advanced Routing + +**Routing** refers to the definition of application end points (URIs) and how +they respond to client requests. For an introduction to routing, see the Basic routing +section. + +The following code is an example of a very basic route. + +~~~ +import popcorn + +class HelloHandler + super Handler + + redef fun get(req, res) do res.send "Hello World!" +end + +var app = new App +app.use("/", new HelloHandler) +~~~ + +### Route methods + +A **route method** is derived from one of the HTTP methods, and is attached to an +instance of the Handler class. + +The following code is an example of routes that are defined for the GET and the POST +methods to the root of the app. + +~~~ +import popcorn + +class GetPostHandler + super Handler + + redef fun get(req, res) do res.send "GET request to the homepage" + redef fun post(req, res) do res.send "POST request to the homepage" +end + +var app = new App +app.use("/", new GetPostHandler) +~~~ + +Popcorn supports the following routing methods that correspond to HTTP methods: +get, post, put, and delete. + +The request query string is accessed through the `req` parameter: + +~~~ +import popcorn +import template + +class QueryStringHandler + super Handler + + redef fun get(req, res) do + var tpl = new Template + tpl.addn "URI: {req.uri}" + tpl.addn "Query string: {req.query_string}" + for name, arg in req.get_args do + tpl.addn "{name}: {arg}" + end + res.send tpl + end +end + +var app = new App +app.use("/", new QueryStringHandler) +app.listen("localhost", 3000) +~~~ + +Post parameters can also be accessed through the `req` parameter: + +~~~ +import popcorn +import template + +class PostHandler + super Handler + + redef fun post(req, res) do + var tpl = new Template + tpl.addn "URI: {req.uri}" + tpl.addn "Body: {req.body}" + for name, arg in req.post_args do + tpl.addn "{name}: {arg}" + end + res.send tpl + end +end + +var app = new App +app.use("/", new PostHandler) +app.listen("localhost", 3000) +~~~ + +There is a special routing method, `all(res, req)`, which is not derived from any +HTTP method. This method is used to respond at a path for all request methods. + +In the following example, the handler will be executed for requests to "/user" +whether you are using GET, POST, PUT, DELETE, or any other HTTP request method. + +~~~ +import popcorn + +class AllHandler + super Handler + + redef fun all(req, res) do res.send "Every request to the homepage" +end +~~~ + +Using the `all` method you can also implement other HTTP request methods. + +~~~ +import popcorn + +class MergeHandler + super Handler + + redef fun all(req, res) do + if req.method == "MERGE" then + # handle that method + else super # keep handle GET, POST, PUT and DELETE methods + end +end +~~~ + +### Route paths + +**Route paths**, in combination with a request handlers, define the endpoints at +which requests can be made. +Route paths can be strings, parameterized strings or glob patterns. +Query strings such as `?q=foo`are not part of the route path. + +Popcorn uses the `Handler::match(uri)` method to match the route paths. + +Here are some examples of route paths based on strings. + +This route path will match requests to the root route, `/`. + +~~~ +import popcorn + +class MyHandler + super Handler + + redef fun get(req, res) do res.send "Got a GET request" +end + +var app = new App +app.use("/", new MyHandler) +~~~ + +This route path will match requests to `/about`. + +~~~ +app.use("/about", new MyHandler) +~~~ + +This route path will match requests to `/random.text`. + +~~~ +app.use("/random.text", new MyHandler) +~~~ + +During the query/response process, routes are matched by order of declaration +through the `App::use` method. + +The app declared in this example will try to match the routes in this order: + +1. `/` +2. `/about` +3. `/random.text` + +### Route parameters + +**Route parameters** are variable parts of a route path. They can be used to path +arguments within the URI. +Parameters in a route are prefixed with a colon `:` like in `:userId`, `:year`. + +The following example declares a handler `UserHome` that responds with the `user` +name. + +~~~ +import popcorn + +class UserHome + super Handler + + redef fun get(req, res) do + var user = req.param("user") + if user != null then + res.send "Hello {user}" + else + res.send("Nothing received", 400) + end + end +end + +var app = new App +app.use("/:user", new UserHome) +app.listen("localhost", 3000) +~~~ + +The `UserHome` handler listen to every path matching `/:user`. This can be `/Morriar`, +`/10`, ... but not `/Morriar/profile` since route follow the strict matching rule. + +### Glob routes + +**Glob routes** are routes that match only on a prefix, thus accepting a wider range +of URI. +Glob routes end with the symbol `*`. + +Here we define a `UserItem` handler that will respond to any URI matching the prefix +`/user/:user/item/:item`. +Note that glob route are compatible with route parameters. + +~~~ +import popcorn + +class UserItem + super Handler + + redef fun get(req, res) do + var user = req.param("user") + var item = req.param("item") + if user == null or item == null then + res.send("Nothing received", 400) + else + res.send "Here the item {item} of the use {user}." + end + end +end + +var app = new App +app.use("/user/:user/item/:item/*", new UserItem) +app.listen("localhost", 3000) +~~~ + +## Response methods + +The methods on the response object (`res`), can is used to manipulate the +request-response cycle. +If none of these methods are called from a route handler, the client request will +receive a `404 Not found` error. + +* `res.html()` Send a HTML response. +* `res.json()` Send a JSON response. +* `res.redirect()` Redirect a request. +* `res.send()` Send a response of various types. +* `res.error()` Set the response status code and send its message as the response body. + +## Middlewares + +### Overview + +**Middleware** handlers are handlers that typically do not send `HttpResponse` responses. +Middleware handlers can perform the following tasks: + +* Execute any code. +* Make changes to the request and the response objects. +* End its action and pass to the next handler in the stack. + +If a middleware handler makes a call to `res.send()`, it provoques the end the +request-response cycle and the response is sent to the client. + +### Ultra simple logger example + +Here is an example of a simple “Hello World” Popcorn application. +We add a middleware handler to the application called MyLogger that prints a simple +log message in the app stdout. + +~~~ +import popcorn + +class MyLogger + super Handler + + redef fun all(req, res) do print "Request Logged!" +end + +class HelloHandler + super Handler + + redef fun get(req, res) do res.send "Hello World!" +end + + +var app = new App +app.use("/*", new MyLogger) +app.use("/", new HelloHandler) +app.listen("localhost", 3000) +~~~ + +By using the `MyLogger` handler to the route `/*` we ensure that every requests +(even 404 ones) pass through the middleware handler. +This handler just prints “Request Logged!” when a request is received. + +The order of middleware loading is important: middleware functions that are loaded first are also executed first. +In the above example, `MyLogger` will be executed before `HelloHandler`. + +### Ultra cool, more advanced logger example + +Next, we’ll create a middleware handler called “LogHandler” that prints the requested +uri, the response status and the time it took to Popcorn to process the request. + +This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares. + +~~~ +import popcorn +import realtime + +redef class HttpRequest + # Time that request was received by the Popcorn app. + var timer: nullable Clock = null +end + +class RequestTimeHandler + super Handler + + redef fun all(req, res) do req.timer = new Clock +end + +class LogHandler + super Handler + + redef fun all(req, res) do + var timer = req.timer + if timer != null then + print "{req.method} {req.uri} {res.color_status} ({timer.total}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("/*", new RequestTimeHandler) +app.use("/", new HelloHandler) +app.use("/*", new LogHandler) +app.listen("localhost", 3000) +~~~ + +First, we attach a new attribute `timer` to every `HttpRequest`. +Doing so we can access our data from all handlers that import our module, directly +from the `req` parameter. + +We use the new middleware called `RequestTimeHandler` to initialize the request timer. + +Finally, our `LogHandler` will display a bunch of data and use the request `timer` +to display the time it took to process the request. + +The app now uses the `RequestTimeHandler` middleware for every requests received +by the Popcorn app. +The page is processed the `HelloHandler` to display the index page. +And, before every response is sent, the `LogHandler` is activated to display the +log line. + +Because you have access to the request object, the response object, and all the +Popcorn API, the possibilities with middleware functions are endless. + +### Built-in middlewares + +Starting with version 0.1, Popcorn provide a set of built-in middleware that can +be used to develop your app faster. + +* `RequestClock`: initializes requests clock. +* `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`). +* `SessionInit`: initializes requests session (see the `Sessions` section). +* `StaticServer`: serves static files (see the `Serving static files with Popcorn` section). +* `Router`: a mountable mini-app (see the `Mountable routers` section). + +## Mountable routers + +Use the `Router` class to create modular, mountable route handlers. +A Router instance is a complete middleware and routing system; for this reason, +it is often referred to as a “mini-app”. + +The following example creates a router as a module, loads a middleware handler in it, +defines some routes, and mounts the router module on a path in the main app. + +~~~ +import popcorn + +class AppHome + super Handler + + redef fun get(req, res) do res.send "Site Home" +end + +class UserLogger + super Handler + + redef fun all(req, res) do print "User logged" +end + +class UserHome + super Handler + + redef fun get(req, res) do res.send "User Home" +end + +class UserProfile + super Handler + + redef fun get(req, res) do res.send "User Profile" +end + +var user_router = new Router +user_router.use("/*", new UserLogger) +user_router.use("/", new UserHome) +user_router.use("/profile", new UserProfile) + +var app = new App +app.use("/", new AppHome) +app.use("/user", user_router) +app.listen("localhost", 3000) +~~~ + +The app will now be able to handle requests to /user and /user/profile, as well +as call the `Time` middleware handler that is specific to the route. + +## Error handling + +**Error handling** is based on middleware handlers. + +Define error-handling middlewares in the same way as other middleware handlers: + +~~~ +import popcorn + +class SimpleErrorHandler + super Handler + + redef fun all(req, res) do + if res.status_code != 200 then + print "An error occurred! {res.status_code})" + end + end +end + +class HelloHandler + super Handler + + redef fun get(req, res) do res.send "Hello World!" +end + +var app = new App +app.use("/", new HelloHandler) +app.use("/*", new SimpleErrorHandler) +app.listen("localhost", 3000) +~~~ + +In this example, every non-200 response is caught by the `SimpleErrorHandler` +that print an error in stdout. + +By defining multiple middleware error handlers, you can take multiple action depending +on the kind of error or the kind of interface you provide (HTML, XML, JSON...). + +Here an example of the 404 custom error page in HTML: + +~~~ +import popcorn +import template + +class HtmlErrorTemplate + super Template + + var status: Int + var message: nullable String + + redef fun rendering do add """ + + + + + {{{message or else status}}} + + +

{{{status}}} {{{message or else ""}}}

+ + """ +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 """ +

Is logged: {{{req.session.as(not null).is_logged}}}

+
+ +
""" + 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 "

Users

" + tpl.add "" + for user in users do + tpl.add """ + + + """ + end + tpl.add "
{{{user["login"] or else "null"}}}{{{user["password"] or else "null"}}}
" + res.html tpl + end +end + +class UserForm + super Handler + + var db: MongoDb + + redef fun get(req, res) do + var tpl = new Template + tpl.add """

Add a new user

+
+ + + +
""" + res.html tpl + end + + redef fun post(req, res) do + var json = new JsonObject + json["login"] = req.post_args["login"] + json["password"] = req.post_args["password"] + db.collection("users").insert(json) + res.redirect "/" + end +end + +var mongo = new MongoClient("mongodb://localhost:27017/") +var db = mongo.database("mongo_example") + +var app = new App +app.use("/", new UserList(db)) +app.use("/new", new UserForm(db)) +app.listen("localhost", 3000) +~~~ + +## Angular.JS integration + +Loving [AngularJS](https://angularjs.org/)? Popcorn is made for Angular and for you! + +Using the StaticHandler with a glob route, you can easily redirect all HTTP requests +to your angular controller: + +~~~ +import popcorn + +var app = new App +app.use("/*", new StaticHandler("my-ng-app/")) +app.listen("localhost", 3000) +~~~ + +See the examples for a more detailed use case working with a JSON API. diff --git a/lib/popcorn/examples/angular/example_angular.nit b/lib/popcorn/examples/angular/example_angular.nit new file mode 100644 index 0000000..ba7f4f2 --- /dev/null +++ b/lib/popcorn/examples/angular/example_angular.nit @@ -0,0 +1,44 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014-2015 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +class CounterAPI + super Handler + + var counter = 0 + + fun json_counter: JsonObject do + var json = new JsonObject + json["label"] = "Visitors" + json["value"] = counter + return json + end + + redef fun get(req, res) do + res.json(json_counter) + end + + redef fun post(req, res) do + counter += 1 + res.json(json_counter) + end +end + +var app = new App +app.use("/counter", new CounterAPI) +app.use("/*", new StaticHandler("www/")) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/angular/www/index.html b/lib/popcorn/examples/angular/www/index.html new file mode 100644 index 0000000..59bc4d1 --- /dev/null +++ b/lib/popcorn/examples/angular/www/index.html @@ -0,0 +1,14 @@ + + + + + ng-example + + +
+ + + + + + diff --git a/lib/popcorn/examples/angular/www/javascripts/ng-example.js b/lib/popcorn/examples/angular/www/javascripts/ng-example.js new file mode 100644 index 0000000..f9270d2 --- /dev/null +++ b/lib/popcorn/examples/angular/www/javascripts/ng-example.js @@ -0,0 +1,73 @@ +/* + * Copyright 2016 Alexandre Terrasa . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + angular + + .module('ng-example', ['ngRoute']) + + .factory('CounterModel', ['$http', function($http) { + return { + load: function(cb, cbErr) { + $http.get('/counter') + .success(cb) + .error(cbErr); + }, + increment: function(cb, cbErr) { + $http.post('/counter') + .success(cb) + .error(cbErr); + } + }; + }]) + + .controller('CounterCtrl', ['CounterModel', function(CounterModel) { + var $this = this; + + this.loadCounter = function() { + CounterModel.load( + function(data) { + $this.counter = data; + }, function(err) { + $this.error = err; + }); + }; + + this.incrementCounter = function() { + CounterModel.increment( + function(data) { + $this.counter = data; + }, function(err) { + $this.error = err; + }); + }; + + this.loadCounter(); + }]) + + .config(function($routeProvider, $locationProvider) { + $routeProvider + .when('/', { + templateUrl: 'views/index.html', + controller: 'CounterCtrl', + controllerAs: 'counterCtrl' + }) + .otherwise({ + redirectTo: '/' + }); + $locationProvider.html5Mode(true); + }); +})(); diff --git a/lib/popcorn/examples/angular/www/views/index.html b/lib/popcorn/examples/angular/www/views/index.html new file mode 100644 index 0000000..058e799 --- /dev/null +++ b/lib/popcorn/examples/angular/www/views/index.html @@ -0,0 +1,9 @@ +

Nit ♥ Angular.JS

+ +

Click the button to increment the counter.

+ +
+ + + +
diff --git a/lib/popcorn/examples/handlers/example_post_handler.nit b/lib/popcorn/examples/handlers/example_post_handler.nit new file mode 100644 index 0000000..826c165 --- /dev/null +++ b/lib/popcorn/examples/handlers/example_post_handler.nit @@ -0,0 +1,36 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn +import template + +class PostHandler + super Handler + + redef fun post(req, res) do + var tpl = new Template + tpl.addn "URI: {req.uri}" + tpl.addn "Body: {req.body}" + for name, arg in req.post_args do + tpl.addn "{name}: {arg}" + end + res.send tpl + end +end + +var app = new App +app.use("/", new PostHandler) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/handlers/example_query_string.nit b/lib/popcorn/examples/handlers/example_query_string.nit new file mode 100644 index 0000000..e8c6187 --- /dev/null +++ b/lib/popcorn/examples/handlers/example_query_string.nit @@ -0,0 +1,36 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn +import template + +class QueryStringHandler + super Handler + + redef fun get(req, res) do + var tpl = new Template + tpl.addn "URI: {req.uri}" + tpl.addn "Query string: {req.query_string}" + for name, arg in req.get_args do + tpl.addn "{name}: {arg}" + end + res.send tpl + end +end + +var app = new App +app.use("/", new QueryStringHandler) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/hello_world/example_hello.nit b/lib/popcorn/examples/hello_world/example_hello.nit new file mode 100644 index 0000000..fc9f3e5 --- /dev/null +++ b/lib/popcorn/examples/hello_world/example_hello.nit @@ -0,0 +1,27 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 "

Hello World!

" +end + +var app = new App +app.use("/", new HelloHandler) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/middlewares/example_advanced_logger.nit b/lib/popcorn/examples/middlewares/example_advanced_logger.nit new file mode 100644 index 0000000..7f8e3b1 --- /dev/null +++ b/lib/popcorn/examples/middlewares/example_advanced_logger.nit @@ -0,0 +1,54 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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("/*", new RequestTimeHandler) +app.use("/", new HelloHandler) +app.use("/*", new LogHandler) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/middlewares/example_html_error_handler.nit b/lib/popcorn/examples/middlewares/example_html_error_handler.nit new file mode 100644 index 0000000..5bd399c --- /dev/null +++ b/lib/popcorn/examples/middlewares/example_html_error_handler.nit @@ -0,0 +1,51 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 """ + + + + + {{{message or else status}}} + + +

{{{status}}} {{{message or else ""}}}

+ + """ +end + +class HtmlErrorHandler + super Handler + + redef fun all(req, res) do + if res.status_code != 200 then + res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!")) + end + end +end + +var app = new App +app.use("/*", new HtmlErrorHandler) +app.listen("localhost", 3000) diff --git a/tests/test_realtime.nit b/lib/popcorn/examples/middlewares/example_simple_error_handler.nit similarity index 58% rename from tests/test_realtime.nit rename to lib/popcorn/examples/middlewares/example_simple_error_handler.nit index 6101a3d..16a5977 100644 --- a/tests/test_realtime.nit +++ b/lib/popcorn/examples/middlewares/example_simple_error_handler.nit @@ -1,6 +1,6 @@ # This file is part of NIT ( http://www.nitlanguage.org ). # -# Copyright 2012 Alexis Laferrière +# Copyright 2016 Alexandre Terrasa # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,30 +14,26 @@ # 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) diff --git a/lib/popcorn/examples/middlewares/example_simple_logger.nit b/lib/popcorn/examples/middlewares/example_simple_logger.nit new file mode 100644 index 0000000..98be552 --- /dev/null +++ b/lib/popcorn/examples/middlewares/example_simple_logger.nit @@ -0,0 +1,35 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +class LogHandler + super Handler + + redef fun all(req, res) do print "Request Logged" +end + +class HelloHandler + super Handler + + redef fun get(req, res) do res.send "Hello World!" +end + + +var app = new App +app.use("/*", new LogHandler) +app.use("/", new HelloHandler) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/mongodb/example_mongodb.nit b/lib/popcorn/examples/mongodb/example_mongodb.nit new file mode 100644 index 0000000..4f89f5b --- /dev/null +++ b/lib/popcorn/examples/mongodb/example_mongodb.nit @@ -0,0 +1,66 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014-2015 Alexandre Terrasa +# +# 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 """ +

Users

+ +

Add a new user

+
+ + + +
+ +

All users

+ """ + for user in users do + tpl.add """ + + + """ + end + tpl.add "
{{{user["login"] or else "null"}}}{{{user["password"] or else "null"}}}
" + res.html(tpl) + end + + redef fun post(req, res) do + var json = new JsonObject + json["login"] = req.post_args["login"] + json["password"] = req.post_args["password"] + db.collection("users").insert(json) + res.redirect("/") + end +end + +var mongo = new MongoClient("mongodb://localhost:27017/") +var db = mongo.database("mongo_example") + +var app = new App +app.use("/", new UserList(db)) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/routing/example_glob_route.nit b/lib/popcorn/examples/routing/example_glob_route.nit new file mode 100644 index 0000000..b33caec --- /dev/null +++ b/lib/popcorn/examples/routing/example_glob_route.nit @@ -0,0 +1,35 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +class UserItem + super Handler + + redef fun get(req, res) do + var user = req.param("user") + var item = req.param("item") + if user == null or item == null then + res.send("Nothing received", 400) + else + res.send "Here the item {item} of the use {user}." + end + end +end + +var app = new App +app.use("/user/:user/item/:item/*", new UserItem) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/routing/example_param_route.nit b/lib/popcorn/examples/routing/example_param_route.nit new file mode 100644 index 0000000..f536771 --- /dev/null +++ b/lib/popcorn/examples/routing/example_param_route.nit @@ -0,0 +1,34 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +class UserHome + super Handler + + redef fun get(req, res) do + var user = req.param("user") + if user != null then + res.send "Hello {user}" + else + res.send("Nothing received", 400) + end + end +end + +var app = new App +app.use("/:user", new UserHome) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/routing/example_router.nit b/lib/popcorn/examples/routing/example_router.nit new file mode 100644 index 0000000..91aa3ab --- /dev/null +++ b/lib/popcorn/examples/routing/example_router.nit @@ -0,0 +1,51 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +class AppHome + super Handler + + redef fun get(req, res) do res.send "Site Home" +end + +class UserLogger + super Handler + + redef fun all(req, res) do print "User logged" +end + +class UserHome + super Handler + + redef fun get(req, res) do res.send "User Home" +end + +class UserProfile + super Handler + + redef fun get(req, res) do res.send "User Profile" +end + +var user_router = new Router +user_router.use("/*", new UserLogger) +user_router.use("/", new UserHome) +user_router.use("/profile", new UserProfile) + +var app = new App +app.use("/", new AppHome) +app.use("/user", user_router) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/sessions/example_session.nit b/lib/popcorn/examples/sessions/example_session.nit new file mode 100644 index 0000000..be79fb1 --- /dev/null +++ b/lib/popcorn/examples/sessions/example_session.nit @@ -0,0 +1,43 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 """ +

Is logged: {{{req.session.as(not null).is_logged}}}

+
+ +
""" + end + + redef fun post(req, res) do + req.session.as(not null).is_logged = true + res.redirect("/") + end +end + +var app = new App +app.use("/*", new SessionInit) +app.use("/", new AppLogin) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/static_files/example_static.nit b/lib/popcorn/examples/static_files/example_static.nit new file mode 100644 index 0000000..c3da6d0 --- /dev/null +++ b/lib/popcorn/examples/static_files/example_static.nit @@ -0,0 +1,21 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +var app = new App +app.use("/", new StaticHandler("public/")) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/static_files/example_static_multiple.nit b/lib/popcorn/examples/static_files/example_static_multiple.nit new file mode 100644 index 0000000..1c730c6 --- /dev/null +++ b/lib/popcorn/examples/static_files/example_static_multiple.nit @@ -0,0 +1,24 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +var app = new App +app.use("/", new StaticHandler("public/")) +app.use("/", new StaticHandler("files/")) +app.use("/static", new StaticHandler("public/")) +app.use("/static", new StaticHandler("files/")) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/static_files/files/index.html b/lib/popcorn/examples/static_files/files/index.html new file mode 100644 index 0000000..bb850bd --- /dev/null +++ b/lib/popcorn/examples/static_files/files/index.html @@ -0,0 +1,6 @@ + + + +

Another Index

+ + diff --git a/lib/popcorn/examples/static_files/public/css/style.css b/lib/popcorn/examples/static_files/public/css/style.css new file mode 100644 index 0000000..5ba0171 --- /dev/null +++ b/lib/popcorn/examples/static_files/public/css/style.css @@ -0,0 +1,4 @@ +body { + color: blue; + padding: 20px; +} diff --git a/lib/popcorn/examples/static_files/public/hello.html b/lib/popcorn/examples/static_files/public/hello.html new file mode 100644 index 0000000..61521ac --- /dev/null +++ b/lib/popcorn/examples/static_files/public/hello.html @@ -0,0 +1,17 @@ + + + + + + Some Popcorn love + + + + +

Hello Popcorn!

+ + maybe it's a kitten? + + + + diff --git a/lib/popcorn/examples/static_files/public/images/trollface.jpg b/lib/popcorn/examples/static_files/public/images/trollface.jpg new file mode 100644 index 0000000..0c733bc Binary files /dev/null and b/lib/popcorn/examples/static_files/public/images/trollface.jpg differ diff --git a/lib/popcorn/examples/static_files/public/js/app.js b/lib/popcorn/examples/static_files/public/js/app.js new file mode 100644 index 0000000..4a1510d --- /dev/null +++ b/lib/popcorn/examples/static_files/public/js/app.js @@ -0,0 +1 @@ +alert("Hello World!"); diff --git a/lib/popcorn/package.ini b/lib/popcorn/package.ini new file mode 100644 index 0000000..8f99b9c --- /dev/null +++ b/lib/popcorn/package.ini @@ -0,0 +1,12 @@ +[package] +name=popcorn +tags=web,lib +maintainer=Alexandre Terrasa +license=Apache-2.0 +version=1.0 +[upstream] +browse=https://github.com/nitlang/nit/tree/master/lib/popcorn/ +git=https://github.com/nitlang/nit.git +git.directory=lib/popcorn/ +homepage=http://nitlanguage.org +issues=https://github.com/nitlang/nit/issues diff --git a/lib/popcorn/pop_handlers.nit b/lib/popcorn/pop_handlers.nit new file mode 100644 index 0000000..2c85e15 --- /dev/null +++ b/lib/popcorn/pop_handlers.nit @@ -0,0 +1,428 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Route handlers. +module pop_handlers + +import pop_routes +import json + +# Class handler for a route. +# +# **Routing** refers to determining how an application responds to a client request +# to a particular endpoint, which is a URI (or path) and a specific HTTP request +# method GET, POST, PUT or DELETE (other methods are not suported yet). +# +# Each route can have one or more handler methods, which are executed when the route is matched. +# +# Route handlers definition takes the following form: +# +# ~~~nitish +# class MyHandler +# super Handler +# +# redef fun METHOD(req, res) do end +# end +# ~~~ +# +# Where: +# * `MyHandler` is the name of the handler you will add to the app. +# * `METHOD` can be replaced by `get`, `post`, `put` or `delete`. +# +# The following example responds with `Hello World!` to GET and POST requests: +# +# ~~~ +# class MyHandler +# super Handler +# +# redef fun get(req, res) do res.send "Got a GET request" +# redef fun post(req, res) do res.send "Got a POST request" +# end +# ~~~ +# +# To make your handler responds to a specific route, you have to add it to the app. +# +# Respond to POST request on the root route (`/`), the application's home page: +# +# ~~~ +# var app = new App +# app.use("/", new MyHandler) +# ~~~ +# +# Respond to a request to the `/user` route: +# +# ~~~ +# app.use("/user", new MyHandler) +# ~~~ +abstract class Handler + + # Call `all(req, res)` if `route` matches `uri`. + private fun handle(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do + if route.match(uri) then + if route isa AppParamRoute then + req.uri_params = route.parse_uri_parameters(uri) + end + all(req, res) + end + end + + # Handler to all kind of HTTP request methods. + # + # `all` is a special request handler, which is not derived from any + # HTTP method. This method is used to respond at a path for all request methods. + # + # In the following example, the handler will be executed for requests to "/user" + # whether you are using GET, POST, PUT, DELETE, or any other HTTP request method. + # + # ~~~ + # class AllHandler + # super Handler + # + # redef fun all(req, res) do res.send "Every request to the homepage" + # end + # ~~~ + # + # Using the `all` method you can also implement other HTTP request methods. + # + # ~~~ + # class MergeHandler + # super Handler + # + # redef fun all(req, res) do + # if req.method == "MERGE" then + # # handle that method + # else super # keep handle GET, POST, PUT and DELETE methods + # end + # end + # ~~~ + fun all(req: HttpRequest, res: HttpResponse) do + if req.method == "GET" then + get(req, res) + else if req.method == "POST" then + post(req, res) + else if req.method == "PUT" then + put(req, res) + else if req.method == "DELETE" then + delete(req, res) + else + res.status_code = 405 + end + end + + # GET handler. + # + # Exemple of route responding to GET requests. + # ~~~ + # class GetHandler + # super Handler + # + # redef fun get(req, res) do res.send "GETrequest received" + # end + # ~~~ + fun get(req: HttpRequest, res: HttpResponse) do end + + # POST handler. + # + # Exemple of route responding to POST requests. + # ~~~ + # class PostHandler + # super Handler + # + # redef fun post(req, res) do res.send "POST request received" + # end + # ~~~ + fun post(req: HttpRequest, res: HttpResponse) do end + + # PUT handler. + # + # Exemple of route responding to PUT requests. + # ~~~ + # class PutHandler + # super Handler + # + # redef fun put(req, res) do res.send "PUT request received" + # end + # ~~~ + fun put(req: HttpRequest, res: HttpResponse) do end + + # DELETE handler. + # + # Exemple of route responding to PUT requests. + # ~~~ + # class DeleteHandler + # super Handler + # + # redef fun delete(req, res) do res.send "DELETE request received" + # end + # ~~~ + fun delete(req: HttpRequest, res: HttpResponse) do end +end + +# Static files server. +# +# To serve static files such as images, CSS files, and JavaScript files, use the +# Popcorn built-in handler `StaticHandler`. +# +# Pass the name of the directory that contains the static assets to the StaticHandler +# init method to start serving the files directly. +# For example, use the following code to serve images, CSS files, and JavaScript files +# in a directory named `public`: +# +# ~~~ +# var app = new App +# app.use("/", new StaticHandler("public/")) +# ~~~ +# +# Now, you can load the files that are in the `public` directory: +# +# ~~~raw +# http://localhost:3000/images/trollface.jpg +# http://localhost:3000/css/style.css +# http://localhost:3000/js/app.js +# http://localhost:3000/hello.html +# ~~~ +# +# Popcorn looks up the files relative to the static directory, so the name of the +# static directory is not part of the URL. +# To use multiple static assets directories, add the `StaticHandler` multiple times: +# +# ~~~ +# app.use("/", new StaticHandler("public/")) +# app.use("/", new StaticHandler("files/")) +# ~~~ +# +# Popcorn looks up the files in the order in which you set the static directories +# with the `use` method. +# +# To create a virtual path prefix (where the path does not actually exist in the file system) +# for files that are served by the `StaticHandler`, specify a mount path for the +# static directory, as shown below: +# +# ~~~ +# app.use("/static/", new StaticHandler("public/")) +# ~~~ +# +# Now, you can load the files that are in the public directory from the `/static` +# path prefix. +# +# ~~~raw +# http://localhost:3000/static/images/trollface.jpg +# http://localhost:3000/static/css/style.css +# http://localhost:3000/static/js/app.js +# http://localhost:3000/static/hello.html +# ~~~ +# +# However, the path that you provide to the `StaticHandler` is relative to the +# directory from where you launch your app. +# If you run the app from another directory, it’s safer to use the absolute path of +# the directory that you want to serve. +class StaticHandler + super Handler + + # Static files directory to serve. + var static_dir: String + + # Internal file server used to lookup and render files. + var file_server: FileServer is lazy do + var srv = new FileServer(static_dir) + srv.show_directory_listing = false + return srv + end + + redef fun handle(route, uri, req, res) do + var answer = file_server.answer(req, route.uri_root(uri)) + if answer.status_code == 200 then + res.status_code = answer.status_code + res.header.add_all answer.header + res.files.add_all answer.files + res.send + else if answer.status_code != 404 then + res.status_code = answer.status_code + end + end +end + +# Mountable routers +# +# Use the `Router` class to create modular, mountable route handlers. +# A Router instance is a complete middleware and routing system; for this reason, +# it is often referred to as a “mini-app”. +# +# The following example creates a router as a module, loads a middleware handler in it, +# defines some routes, and mounts the router module on a path in the main app. +# +# ~~~ +# class AppHome +# super Handler +# +# redef fun get(req, res) do res.send "Site Home" +# end +# +# class UserLogger +# super Handler +# +# redef fun all(req, res) do print "User logged" +# end +# +# class UserHome +# super Handler +# +# redef fun get(req, res) do res.send "User Home" +# end +# +# class UserProfile +# super Handler +# +# redef fun get(req, res) do res.send "User Profile" +# end +# +# var user_router = new Router +# user_router.use("/*", new UserLogger) +# user_router.use("/", new UserHome) +# user_router.use("/profile", new UserProfile) +# +# var app = new App +# app.use("/", new AppHome) +# app.use("/user", user_router) +# ~~~ +# +# The app will now be able to handle requests to /user and /user/profile, as well +# as call the `Time` middleware handler that is specific to the route. +class Router + super Handler + + # List of handlers to match with requests. + private var handlers = new Map[AppRoute, Handler] + + # Register a `handler` for a route `path`. + # + # Route paths are matched in registration order. + fun use(path: String, handler: Handler) do + var route + if handler isa Router or handler isa StaticHandler then + route = new AppGlobRoute(path) + else if path.has_suffix("*") then + route = new AppGlobRoute(path) + else + route = new AppParamRoute(path) + end + handlers[route] = handler + end + + redef fun handle(route, uri, req, res) do + if not route.match(uri) then return + for hroute, handler in handlers do + handler.handle(hroute, route.uri_root(uri), req, res) + if res.sent then break + end + end +end + +# Popcorn application. +# +# The `App` is the main point of the application. +# It acts as a `Router` that holds the top level route handlers. +# +# Here an example to create a simple web app with Popcorn: +# +# ~~~ +# import popcorn +# +# class HelloHandler +# super Handler +# +# redef fun get(req, res) do res.html "

Hello World!

" +# end +# +# var app = new App +# app.use("/", new HelloHandler) +# # app.listen("localhost", 3000) +# ~~~ +# +# The Popcorn app listens on port 3000 for connections. +# The app responds with "Hello World!" for request to the root URL (`/`) or **route**. +# For every other path, it will respond with a **404 Not Found**. +# +# The `req` (request) and `res` (response) parameters are the same that nitcorn provides +# so you can do anything else you would do in your route without Popcorn involved. +# +# Run the app with the following command: +# +# ~~~bash +# nitc app.nit && ./app +# ~~~ +# +# Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output. +class App + super Router +end + +redef class HttpResponse + + # Was this request sent by a handler? + var sent = false + + private fun check_sent do + if sent then print "Warning: Headers already sent!" + end + + # Write data in body response and send it. + fun send(raw_data: nullable Writable, status: nullable Int) do + if raw_data != null then + body += raw_data.write_to_string + end + if status != null then + status_code = status + else + status_code = 200 + end + check_sent + sent = true + end + + # Write data as HTML and set the right content type header. + fun html(html: nullable Writable, status: nullable Int) do + header["Content-Type"] = media_types["html"].as(not null) + send(html, status) + end + + # Write data as JSON and set the right content type header. + fun json(json: nullable Jsonable, status: nullable Int) do + header["Content-Type"] = media_types["json"].as(not null) + if json == null then + send(null, status) + else + send(json.to_json, status) + end + end + + # Redirect response to `location` + fun redirect(location: String, status: nullable Int) do + header["Location"] = location + if status != null then + status_code = status + else + status_code = 302 + end + check_sent + sent = true + end + + # TODO The error message should be parameterizable. + fun error(status: Int) do + html("Error", status) + end +end diff --git a/lib/popcorn/pop_middlewares.nit b/lib/popcorn/pop_middlewares.nit new file mode 100644 index 0000000..518d9dc --- /dev/null +++ b/lib/popcorn/pop_middlewares.nit @@ -0,0 +1,77 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# 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 diff --git a/lib/popcorn/pop_routes.nit b/lib/popcorn/pop_routes.nit new file mode 100644 index 0000000..68c3d37 --- /dev/null +++ b/lib/popcorn/pop_routes.nit @@ -0,0 +1,263 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Internal routes representation. +module pop_routes + +import nitcorn + +# AppRoute provide services for path and uri manipulation and matching.. +# +# Default strict routes like `/` or `/user` match the same URI string. +# An exception is done for the trailing `/`, which is always omitted during the +# parsing. +# +# ~~~ +# var route = new AppRoute("/") +# assert route.match("") +# assert route.match("/") +# assert not route.match("/user") +# assert not route.match("user") +# +# route = new AppRoute("/user") +# assert not route.match("/") +# assert route.match("/user") +# assert route.match("/user/") +# assert not route.match("/user/10") +# assert not route.match("/foo") +# assert not route.match("user") +# assert not route.match("/username") +# ~~~ +class AppRoute + + # Route relative path from server root. + var path: String + + # Does self match the `req`? + fun match(uri: String): Bool do + uri = uri.simplify_path + var path = resolve_path(uri) + if uri.is_empty and path == "/" then return true + return uri == path + end + + # Replace path parameters with concrete values from the `uri`. + # + # For strict routes, it returns the path unchanged: + # ~~~ + # var route = new AppRoute("/") + # assert route.resolve_path("/user/10/profile") == "/" + # + # route = new AppRoute("/user") + # assert route.resolve_path("/user/10/profile") == "/user" + # ~~~ + fun resolve_path(uri: String): String do return path.simplify_path + + # Remove `resolved_path` prefix from `uri`. + # + # Mainly used to resolve and match mountable routes. + # + # ~~~ + # var route = new AppRoute("/") + # assert route.uri_root("/user/10/profile") == "/user/10/profile" + # + # route = new AppRoute("/user") + # assert route.uri_root("/user/10/profile") == "/10/profile" + # ~~~ + fun uri_root(uri: String): String do + var path = resolve_path(uri) + if path == "/" then return uri + return uri.substring(path.length, uri.length).simplify_path + end +end + +# Parameterizable routes. +# +# Routes that can contains variables parts that will be resolved during the +# matching process. +# +# Route parameters are marked with a colon `:` +# ~~~ +# var route = new AppParamRoute("/:id") +# assert not route.match("/") +# assert route.match("/user") +# assert route.match("/user/") +# assert not route.match("/user/10") +# ~~~ +# +# It is possible to use more than one parameter in the same route: +# ~~~ +# route = new AppParamRoute("/user/:userId/items/:itemId") +# assert not route.match("/user/10/items/") +# assert route.match("/user/10/items/e895346") +# assert route.match("/user/USER/items/0/") +# assert not route.match("/user/10/items/10/profile") +# ~~~ +class AppParamRoute + super AppRoute + + init do parse_path_parameters(path) + + # Cut `path` into `UriParts`. + fun parse_path_parameters(path: String) do + for part in path.split("/") do + if not part.is_empty and part.first == ':' then + # is an uri param + path_parts.add new UriParam(part.substring(1, part.length)) + else + # is a standard string + path_parts.add new UriString(part) + end + end + end + + # For parameterized routes, parameter names are replaced by their value in the URI. + # ~~~ + # var route = new AppParamRoute("/user/:id") + # assert route.resolve_path("/user/10/profile") == "/user/10" + # + # route = new AppParamRoute("/user/:userId/items/:itemId") + # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156" + # ~~~ + redef fun resolve_path(uri) do + var uri_params = parse_uri_parameters(uri) + var path = "/" + for part in path_parts do + if part isa UriString then + path /= part.string + else if part isa UriParam then + path /= uri_params.get_or_default(part.name, part.name) + end + end + return path.simplify_path + end + + # Extract parameter values from `uri`. + # ~~~ + # var route = new AppParamRoute("/user/:userId/items/:itemId") + # var params = route.parse_uri_parameters("/user/10/items/i125/desc") + # assert params["userId"] == "10" + # assert params["itemId"] == "i125" + # assert params.length == 2 + # + # params = route.parse_uri_parameters("/") + # assert params.is_empty + # ~~~ + fun parse_uri_parameters(uri: String): Map[String, String] do + var res = new HashMap[String, String] + if path_parts.is_empty then return res + var parts = uri.split("/") + for i in [0 .. path_parts.length[ do + if i >= parts.length then return res + var ppart = path_parts[i] + var part = parts[i] + if not ppart.match(part) then return res + if ppart isa UriParam then + res[ppart.name] = part + end + end + return res + end + + private var path_parts = new Array[UriPart] +end + +# Route with glob. +# +# Route variable part is suffixed with a star `*`: +# ~~~ +# var route = new AppGlobRoute("/*") +# assert route.match("/") +# assert route.match("/user") +# assert route.match("/user/10") +# ~~~ +# +# Glob routes can be combined with parameters: +# ~~~ +# route = new AppGlobRoute("/user/:id/*") +# assert not route.match("/user") +# assert route.match("/user/10") +# assert route.match("/user/10/profile") +# ~~~ +# +# Note that the star can be used directly on the end of an URI part: +# ~~~ +# route = new AppGlobRoute("/user*") +# assert route.match("/user") +# assert route.match("/username") +# assert route.match("/user/10/profile") +# assert not route.match("/foo") +# ~~~ +# +# For now, stars cannot be used inside a route, use URI parameters instead. +class AppGlobRoute + super AppParamRoute + + # Path without the trailing `*`. + # ~~~ + # var route = new AppGlobRoute("/user/:id/*") + # assert route.resolve_path("/user/10/profile") == "/user/10" + # + # route = new AppGlobRoute("/user/:userId/items/:itemId*") + # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156" + # ~~~ + redef fun resolve_path(uri) do + var path = super + if path.has_suffix("*") then + return path.substring(0, path.length - 1).simplify_path + end + return path.simplify_path + end + + redef fun match(uri) do + var path = resolve_path(uri) + return uri.has_prefix(path.substring(0, path.length - 1)) + end +end + +# A String that compose an URI. +# +# In practice, UriPart can be parameters or static strings. +private interface UriPart + # Does `self` matches a part of the uri? + fun match(uri_part: String): Bool is abstract +end + +# An uri parameter string like `:id`. +private class UriParam + super UriPart + + # Param `name` in the route uri. + var name: String + + # Parameters match everything. + redef fun match(part) do return not part.is_empty + + redef fun to_s do return name +end + +# A static uri string like `users`. +private class UriString + super UriPart + + # Uri part string. + var string: String + + # Empty strings match everything otherwise matching is based on string equality. + redef fun match(part) do return string.is_empty or string == part + + redef fun to_s do return string +end diff --git a/lib/popcorn/popcorn.nit b/lib/popcorn/popcorn.nit new file mode 100644 index 0000000..887c122 --- /dev/null +++ b/lib/popcorn/popcorn.nit @@ -0,0 +1,89 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Application server abstraction on top of nitcorn. +module popcorn + +import nitcorn +import pop_middlewares +intrude import pop_handlers + +# App acts like a wrapper around a nitcorn `Action`. +redef class App + super Action + + # Do not show anything on console + var quiet = false is writable + + # Start listening on `host:port`. + fun listen(host: String, port: Int) do + var iface = "{host}:{port}" + var vh = new VirtualHost(iface) + + vh.routes.add new Route("/", self) + + var fac = new HttpFactory.and_libevent + fac.config.virtual_hosts.add vh + + if not quiet then + print "Launching server on http://{iface}/" + end + fac.run + end + + # Handle request from nitcorn + redef fun answer(req, uri) do + uri = uri.simplify_path + var res = new HttpResponse(404) + for route, handler in handlers do + handler.handle(route, uri, req, res) + end + if not res.sent then + res.send(error_tpl(res.status_code, res.status_message), 404) + end + res.session = req.session + return res + end + + # + fun error_tpl(status: Int, message: nullable String): Template do + return new ErrorTpl(status, message) + end +end + +# +class ErrorTpl + super Template + + # + var status: Int + + # + var message: nullable String + + redef fun rendering do add """ + + + + + {{{message or else status}}} + + +

{{{status}}} {{{message or else ""}}}

+ + """ + +end diff --git a/lib/popcorn/test_pop_routes.nit b/lib/popcorn/test_pop_routes.nit new file mode 100644 index 0000000..9ad37a5 --- /dev/null +++ b/lib/popcorn/test_pop_routes.nit @@ -0,0 +1,166 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module test_pop_routes is test_suite + +import pop_routes +import test_suite + +class TestAppRoute + super TestSuite + + fun test_root_match_only_one_uri do + var r = new AppRoute("/") + assert r.match("") + assert r.match("/") + assert not r.match("/user") + end + + fun test_strict_route_match_only_one_uri do + var r = new AppRoute("/user") + assert not r.match("/") + assert r.match("/user") + assert r.match("/user/") + assert not r.match("/user/10") + assert not r.match("/foo") + end +end + +class TestAppParamRoute + super TestSuite + + fun test_param_route_match_good_uri_params_1 do + var r = new AppParamRoute("/:id") + assert not r.match("/") + assert r.match("/user") + assert r.match("/user/") + assert not r.match("/user/10") + end + + fun test_param_route_match_good_uri_params_2 do + var r = new AppParamRoute("/user/:id") + assert not r.match("/") + assert not r.match("/user") + assert not r.match("/user/") + assert r.match("/user/10") + assert r.match("/user/10/") + assert not r.match("/user/10/profile") + end + + fun test_param_route_match_good_uri_params_3 do + var r = new AppParamRoute("/user/:id/profile") + assert not r.match("/") + assert not r.match("/user") + assert not r.match("/user/") + assert not r.match("/user/10") + assert not r.match("/user/10/") + assert r.match("/user/10/profile") + assert r.match("/user/10/profile/") + assert not r.match("/user/10/profile/settings") + assert not r.match("/user/10/foo") + end + + fun test_param_route_match_good_uri_params_4 do + var r = new AppParamRoute("/:id/:foo") + assert not r.match("/") + assert not r.match("/user") + assert not r.match("/user/") + assert r.match("/user/10") + assert r.match("/user/10/") + assert not r.match("/user/10/10") + end + + fun test_param_route_match_good_uri_params_5 do + var r = new AppParamRoute("/user/:id/:foo") + assert not r.match("/") + assert not r.match("/user") + assert not r.match("/foo") + assert not r.match("/user/10") + assert r.match("/user/10/10") + assert r.match("/user/10/10/") + assert not r.match("/user/10/10/profile") + end + + fun test_param_route_match_good_uri_params_6 do + var r = new AppParamRoute("/user/:id/settings/:foo") + assert not r.match("/") + assert not r.match("/user") + assert not r.match("/foo") + assert not r.match("/user/10") + assert not r.match("/user/10/10") + assert not r.match("/user/10/10/") + assert not r.match("/user/10/10/profile") + assert r.match("/user/10/settings/profile") + assert r.match("/user/10/settings/profile/") + assert not r.match("/user/10/settings/profile/10") + end +end + +class TestRouteMatching + super TestSuite + + fun test_glob_route_match_good_uri_prefix1 do + var r = new AppGlobRoute("/*") + assert r.match("/") + assert r.match("/user") + assert r.match("/user/10") + end + + fun test_glob_route_match_good_uri_prefix2 do + var r = new AppGlobRoute("/user/*") + assert not r.match("/") + assert r.match("/user") + assert r.match("/user/10") + end + + fun test_glob_route_match_good_uri_prefix3 do + var r = new AppGlobRoute("/user*") + assert not r.match("/") + assert r.match("/user") + assert r.match("/user/10") + end + + fun test_glob_route_work_with_parameters_1 do + var r = new AppGlobRoute("/:id/*") + assert not r.match("/") + assert r.match("/user") + assert r.match("/user/10") + assert r.match("/user/10/profile") + end + + fun test_glob_route_work_with_parameters_2 do + var r = new AppGlobRoute("/:id*") + assert not r.match("/") + assert r.match("/user") + assert r.match("/user/10") + end + + fun test_glob_route_work_with_parameters_3 do + var r = new AppGlobRoute("/user/:id/*") + assert not r.match("/") + assert not r.match("/user") + assert r.match("/user/10") + assert r.match("/user/10/profile") + end + + fun test_glob_route_work_with_parameters_4 do + var r = new AppGlobRoute("/user/:id*") + assert not r.match("/") + assert not r.match("/user") + assert r.match("/user/10") + assert r.match("/user/10/profile") + end +end diff --git a/lib/popcorn/tests/Makefile b/lib/popcorn/tests/Makefile new file mode 100644 index 0000000..7e78098 --- /dev/null +++ b/lib/popcorn/tests/Makefile @@ -0,0 +1,21 @@ +# Copyright 2013 Alexandre Terrasa . +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +all: tests + +check: clean + ./tests.sh + +clean: + rm -rf out/ diff --git a/lib/popcorn/tests/base_tests.nit b/lib/popcorn/tests/base_tests.nit new file mode 100644 index 0000000..a56a33d --- /dev/null +++ b/lib/popcorn/tests/base_tests.nit @@ -0,0 +1,63 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn +import pthreads + +redef class Sys + var test_host = "localhost" + + # Return a new port for each instance + fun test_port: Int do + srand + return 10000+20000.rand + end +end + +class AppThread + super Thread + + var host: String + var port: Int + var app: App + + redef fun main + do + # Hide testing concept to force nitcorn to actually run + "NIT_TESTING".setenv("false") + app.quiet = true + app.listen(host, port) + return null + end +end + +class ClientThread + super Thread + + var host: String + var port: Int + + redef fun main do return null + + # Regex to catch and hide the port from the output to get consistent results + var host_re: Regex = "localhost:\[0-9\]+".to_re + + fun system(cmd: String, title: nullable String) + do + title = title or else cmd + title = title.replace(host_re, "localhost:*****") + print "\n[Client] {title}" + sys.system cmd + end +end diff --git a/lib/popcorn/tests/res/test_example_advanced_logger.res b/lib/popcorn/tests/res/test_example_advanced_logger.res new file mode 100644 index 0000000..7e56183 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_advanced_logger.res @@ -0,0 +1,16 @@ + +[Client] curl -s localhost:*****/ +GET / 200 (0.0s) +Hello World! +[Client] curl -s localhost:*****/about +GET /about 404 (0.0s) + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_angular.res b/lib/popcorn/tests/res/test_example_angular.res new file mode 100644 index 0000000..c07813f --- /dev/null +++ b/lib/popcorn/tests/res/test_example_angular.res @@ -0,0 +1,7 @@ + +[Client] curl -s localhost:*****/counter +{"label":"Visitors","value":0} +[Client] curl -s localhost:*****/counter -X POST +{"label":"Visitors","value":1} +[Client] curl -s localhost:*****/counter +{"label":"Visitors","value":1} \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_glob_route.res b/lib/popcorn/tests/res/test_example_glob_route.res new file mode 100644 index 0000000..1e91986 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_glob_route.res @@ -0,0 +1,42 @@ + +[Client] curl -s localhost:*****/user/Morriar/item/10 +Here the item 10 of the use Morriar. +[Client] curl -s localhost:*****/user/Morriar/item/10/ +Here the item 10 of the use Morriar. +[Client] curl -s localhost:*****/user/Morriar/item/10/profile +Here the item 10 of the use Morriar. +[Client] curl -s localhost:*****/user/Morriar/item/10/profile/settings +Here the item 10 of the use Morriar. +[Client] curl -s localhost:*****/ + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/not_found/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_hello.res b/lib/popcorn/tests/res/test_example_hello.res new file mode 100644 index 0000000..194fb36 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_hello.res @@ -0,0 +1,29 @@ + +[Client] curl -s localhost:***** +

Hello World!

+[Client] curl -s localhost:*****/ +

Hello World!

+[Client] curl -s localhost:*****/////////// +

Hello World!

+[Client] curl -s localhost:*****/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/not_found/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_html_error_handler.res b/lib/popcorn/tests/res/test_example_html_error_handler.res new file mode 100644 index 0000000..3015280 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_html_error_handler.res @@ -0,0 +1,23 @@ + +[Client] curl -s localhost:*****/ + + + + + An error occurred! + + +

404 An error occurred!

+ + +[Client] curl -s localhost:*****/about + + + + + An error occurred! + + +

404 An error occurred!

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_param_route.res b/lib/popcorn/tests/res/test_example_param_route.res new file mode 100644 index 0000000..f0b898f --- /dev/null +++ b/lib/popcorn/tests/res/test_example_param_route.res @@ -0,0 +1,38 @@ + +[Client] curl -s localhost:*****/Morriar +Hello Morriar +[Client] curl -s localhost:*****// + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/ + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/not_found +Hello not_found +[Client] curl -s localhost:*****/not_found/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_post.res b/lib/popcorn/tests/res/test_example_post.res new file mode 100644 index 0000000..72589e2 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_post.res @@ -0,0 +1,38 @@ + +[Client] curl -s localhost:*****/ -X POST +URI: / +Body: + +[Client] curl -s localhost:*****/ --data 'user' +POST Error: user format error on user +URI: / +Body: user + +[Client] curl -s localhost:*****/ --data 'user=Morriar' +URI: / +Body: user=Morriar +user: Morriar + +[Client] curl -s localhost:*****/ --data 'user=&order=desc' +URI: / +Body: user=&order=desc +user: +order: desc + +[Client] curl -s localhost:*****/ --data 'user=Morriar&order=desc' +URI: / +Body: user=Morriar&order=desc +user: Morriar +order: desc + +[Client] curl -s localhost:*****/ + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_query_string.res b/lib/popcorn/tests/res/test_example_query_string.res new file mode 100644 index 0000000..699b0db --- /dev/null +++ b/lib/popcorn/tests/res/test_example_query_string.res @@ -0,0 +1,24 @@ + +[Client] curl -s localhost:*****/ +URI: / +Query string: + +[Client] curl -s localhost:*****/?user=Morriar +URI: / +Query string: user=Morriar +user: Morriar + +[Client] curl -s localhost:*****/?reload +URI: / +Query string: reload + +[Client] curl -s localhost:*****/?foo\&bar=baz +URI: / +Query string: foo&bar=baz +bar: baz + +[Client] curl -s localhost:*****/?items=10\&order=asc +URI: / +Query string: items=10&order=asc +items: 10 +order: asc diff --git a/lib/popcorn/tests/res/test_example_router.res b/lib/popcorn/tests/res/test_example_router.res new file mode 100644 index 0000000..fb222c3 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_router.res @@ -0,0 +1,48 @@ + +[Client] curl -s localhost:***** +Site Home +[Client] curl -s localhost:*****/ +Site Home +[Client] curl -s localhost:*****/user +User logged +User Home +[Client] curl -s localhost:*****/user/ +User logged +User Home +[Client] curl -s localhost:*****/user/profile +User logged +User Profile +[Client] curl -s localhost:*****/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/user/not_found +User logged + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/products/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_session.res b/lib/popcorn/tests/res/test_example_session.res new file mode 100644 index 0000000..4ca758e --- /dev/null +++ b/lib/popcorn/tests/res/test_example_session.res @@ -0,0 +1,41 @@ + +[Client] curl -s localhost:*****/ +

Is logged: false

+
+ +
+[Client] curl -s localhost:*****/ -X POST + +[Client] curl -s localhost:*****/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/user/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/products/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_simple_error_handler.res b/lib/popcorn/tests/res/test_example_simple_error_handler.res new file mode 100644 index 0000000..67e5a72 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_simple_error_handler.res @@ -0,0 +1,5 @@ + +[Client] curl -s localhost:*****/ +Hello World! +[Client] curl -s localhost:*****/about +An error occurred! \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_simple_logger.res b/lib/popcorn/tests/res/test_example_simple_logger.res new file mode 100644 index 0000000..9bc50e3 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_simple_logger.res @@ -0,0 +1,16 @@ + +[Client] curl -s localhost:*****/ +Request Logged +Hello World! +[Client] curl -s localhost:*****/about +Request Logged + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_static.res b/lib/popcorn/tests/res/test_example_static.res new file mode 100644 index 0000000..7f07ed8 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_static.res @@ -0,0 +1,73 @@ + +[Client] curl -s localhost:*****/css/style.css +body { + color: blue; + padding: 20px; +} + +[Client] curl -s localhost:*****/js/app.js +alert("Hello World!"); + +[Client] curl -s localhost:*****/hello.html + + + + + + Some Popcorn love + + + + +

Hello Popcorn!

+ + maybe it's a kitten? + + + + + +[Client] curl -s localhost:*****/ + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/css/not_found.nit + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/static/css/not_found.nit + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/not_found.nit + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_example_static_multiple.res b/lib/popcorn/tests/res/test_example_static_multiple.res new file mode 100644 index 0000000..1b14f14 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_static_multiple.res @@ -0,0 +1,113 @@ + +[Client] curl -s localhost:*****/css/style.css +body { + color: blue; + padding: 20px; +} + +[Client] curl -s localhost:*****/js/app.js +alert("Hello World!"); + +[Client] curl -s localhost:*****/hello.html + + + + + + Some Popcorn love + + + + +

Hello Popcorn!

+ + maybe it's a kitten? + + + + + +[Client] curl -s localhost:*****/ +Warning: Headers already sent! + + + +

Another Index

+ + + + + +

Another Index

+ + + +[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 + + + + + + Some Popcorn love + + + + +

Hello Popcorn!

+ + maybe it's a kitten? + + + + + +[Client] curl -s localhost:*****/static/ + + + +

Another Index

+ + + +[Client] curl -s localhost:*****/css/not_found.nit + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/static/css/not_found.nit + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/not_found.nit + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_router.res b/lib/popcorn/tests/res/test_router.res new file mode 100644 index 0000000..37be3a6 --- /dev/null +++ b/lib/popcorn/tests/res/test_router.res @@ -0,0 +1,50 @@ + +[Client] curl -s localhost:***** +/ +[Client] curl -s localhost:*****/ +/ +[Client] curl -s localhost:*****/user +/user +[Client] curl -s localhost:*****/user/ +/user +[Client] curl -s localhost:*****/user/settings +/user/settings +[Client] curl -s localhost:*****/products +/products +[Client] curl -s localhost:*****/products/ +/products +[Client] curl -s localhost:*****/products/list +/products/list +[Client] curl -s localhost:*****/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/user/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/products/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/res/test_routes.res b/lib/popcorn/tests/res/test_routes.res new file mode 100644 index 0000000..0c1abfa --- /dev/null +++ b/lib/popcorn/tests/res/test_routes.res @@ -0,0 +1,49 @@ + +[Client] curl -s localhost:***** +/ +[Client] curl -s localhost:*****/ +/ +[Client] curl -s localhost:*****/misc +/misc/everything +[Client] curl -s localhost:*****/misc/foo +/misc/everything +[Client] curl -s localhost:*****/misc/foo/bar +/misc/everything +[Client] curl -s localhost:*****/misc/foo/baz +/misc/everything +[Client] curl -s localhost:*****/user +/user +[Client] curl -s localhost:*****/user/ +/user +[Client] curl -s localhost:*****/user/id +/user/id +[Client] curl -s localhost:*****/user/id/profile +/user/id/profile +[Client] curl -s localhost:*****/user/id/misc/foo +/user/id/misc/everything +[Client] curl -s localhost:*****/user/id/misc/foo/bar +/user/id/misc/everything +[Client] curl -s localhost:*****/user/id/misc/foo/bar/baz +/user/id/misc/everything +[Client] curl -s localhost:*****/not_found + + + + + Not Found + + +

404 Not Found

+ + +[Client] curl -s localhost:*****/user/id/not_found + + + + + Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/lib/popcorn/tests/test_example_advanced_logger.nit b/lib/popcorn/tests/test_example_advanced_logger.nit new file mode 100644 index 0000000..41193d2 --- /dev/null +++ b/lib/popcorn/tests/test_example_advanced_logger.nit @@ -0,0 +1,47 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_advanced_logger +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/about" + return null + end +end + +var app = new App +app.use("/*", new RequestTimeHandler) +app.use("/", new HelloHandler) +app.use("/*", new LogHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_angular.nit b/lib/popcorn/tests/test_example_angular.nit new file mode 100644 index 0000000..d1ab509 --- /dev/null +++ b/lib/popcorn/tests/test_example_angular.nit @@ -0,0 +1,47 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_angular +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/counter" + system "curl -s {host}:{port}/counter -X POST" + system "curl -s {host}:{port}/counter" + return null + end +end + +var app = new App +app.use("/counter", new CounterAPI) +app.use("/*", new StaticHandler("www/")) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_glob_route.nit b/lib/popcorn/tests/test_example_glob_route.nit new file mode 100644 index 0000000..d1ca6c8 --- /dev/null +++ b/lib/popcorn/tests/test_example_glob_route.nit @@ -0,0 +1,51 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_glob_route +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/user/Morriar/item/10" + system "curl -s {host}:{port}/user/Morriar/item/10/" + system "curl -s {host}:{port}/user/Morriar/item/10/profile" + system "curl -s {host}:{port}/user/Morriar/item/10/profile/settings" + + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/not_found/not_found" + return null + end +end + +var app = new App +app.use("/user/:user/item/:item/*", new UserItem) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_hello.nit b/lib/popcorn/tests/test_example_hello.nit new file mode 100644 index 0000000..d7327e3 --- /dev/null +++ b/lib/popcorn/tests/test_example_hello.nit @@ -0,0 +1,48 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_hello +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}" + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}///////////" + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/not_found/not_found" + return null + end +end + +var app = new App +app.use("/", new HelloHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_html_error_handler.nit b/lib/popcorn/tests/test_example_html_error_handler.nit new file mode 100644 index 0000000..95abb7a --- /dev/null +++ b/lib/popcorn/tests/test_example_html_error_handler.nit @@ -0,0 +1,45 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_html_error_handler +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/about" + return null + end +end + +var app = new App +app.use("/*", new HtmlErrorHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_param_route.nit b/lib/popcorn/tests/test_example_param_route.nit new file mode 100644 index 0000000..541fde0 --- /dev/null +++ b/lib/popcorn/tests/test_example_param_route.nit @@ -0,0 +1,49 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_param_route +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/Morriar" + system "curl -s {host}:{port}//" + + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/not_found/not_found" + return null + end +end + +var app = new App +app.use("/:user", new UserHome) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_post.nit b/lib/popcorn/tests/test_example_post.nit new file mode 100644 index 0000000..bf5330e --- /dev/null +++ b/lib/popcorn/tests/test_example_post.nit @@ -0,0 +1,50 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_post_handler +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/ -X POST" + system "curl -s {host}:{port}/ --data 'user'" + system "curl -s {host}:{port}/ --data 'user=Morriar'" + system "curl -s {host}:{port}/ --data 'user=\&order=desc'" + system "curl -s {host}:{port}/ --data 'user=Morriar\&order=desc'" + + system "curl -s {host}:{port}/" + return null + end +end + +var app = new App +app.use("/", new PostHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_query_string.nit b/lib/popcorn/tests/test_example_query_string.nit new file mode 100644 index 0000000..6c489ba --- /dev/null +++ b/lib/popcorn/tests/test_example_query_string.nit @@ -0,0 +1,48 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_query_string +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/?user=Morriar" + system "curl -s {host}:{port}/?reload" + system "curl -s {host}:{port}/?foo\\&bar=baz" + system "curl -s {host}:{port}/?items=10\\&order=asc" + return null + end +end + +var app = new App +app.use("/", new QueryStringHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_router.nit b/lib/popcorn/tests/test_example_router.nit new file mode 100644 index 0000000..f9ab8df --- /dev/null +++ b/lib/popcorn/tests/test_example_router.nit @@ -0,0 +1,58 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_router +import base_tests + +class HelloClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}" + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/user" + system "curl -s {host}:{port}/user/" + system "curl -s {host}:{port}/user/profile" + + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/user/not_found" + system "curl -s {host}:{port}/products/not_found" + return null + end +end + +var user_router = new Router +user_router.use("/*", new UserLogger) +user_router.use("/", new UserHome) +user_router.use("/profile", new UserProfile) + +var app = new App +app.use("/", new AppHome) +app.use("/user", user_router) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new HelloClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_session.nit b/lib/popcorn/tests/test_example_session.nit new file mode 100644 index 0000000..b7bedc3 --- /dev/null +++ b/lib/popcorn/tests/test_example_session.nit @@ -0,0 +1,50 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_session +import base_tests + +class HelloClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/ -X POST" + + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/user/not_found" + system "curl -s {host}:{port}/products/not_found" + return null + end +end + +var app = new App +app.use("/*", new SessionInit) +app.use("/", new AppLogin) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new HelloClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_simple_error_handler.nit b/lib/popcorn/tests/test_example_simple_error_handler.nit new file mode 100644 index 0000000..ce671f0 --- /dev/null +++ b/lib/popcorn/tests/test_example_simple_error_handler.nit @@ -0,0 +1,46 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_simple_error_handler +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/about" + return null + end +end + +var app = new App +app.use("/", new HelloHandler) +app.use("/*", new SimpleErrorHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_simple_logger.nit b/lib/popcorn/tests/test_example_simple_logger.nit new file mode 100644 index 0000000..50f6af9 --- /dev/null +++ b/lib/popcorn/tests/test_example_simple_logger.nit @@ -0,0 +1,46 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import example_simple_logger +import base_tests + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/about" + return null + end +end + +var app = new App +app.use("/*", new LogHandler) +app.use("/", new HelloHandler) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_static.nit b/lib/popcorn/tests/test_example_static.nit new file mode 100644 index 0000000..e23a7be --- /dev/null +++ b/lib/popcorn/tests/test_example_static.nit @@ -0,0 +1,52 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base_tests +import example_static + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/css/style.css" + system "curl -s {host}:{port}/js/app.js" + system "curl -s {host}:{port}/hello.html" + system "curl -s {host}:{port}/" + + system "curl -s {host}:{port}/css/not_found.nit" + system "curl -s {host}:{port}/static/css/not_found.nit" + system "curl -s {host}:{port}/not_found.nit" + + return null + end +end + +var app = new App +app.use("/", new StaticHandler("../examples/static_files/public/")) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_example_static_multiple.nit b/lib/popcorn/tests/test_example_static_multiple.nit new file mode 100644 index 0000000..5e99ae1 --- /dev/null +++ b/lib/popcorn/tests/test_example_static_multiple.nit @@ -0,0 +1,60 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base_tests +import example_static_multiple + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/css/style.css" + system "curl -s {host}:{port}/js/app.js" + system "curl -s {host}:{port}/hello.html" + system "curl -s {host}:{port}/" + + system "curl -s {host}:{port}/static/css/style.css" + system "curl -s {host}:{port}/static/js/app.js" + system "curl -s {host}:{port}/static/hello.html" + system "curl -s {host}:{port}/static/" + + system "curl -s {host}:{port}/css/not_found.nit" + system "curl -s {host}:{port}/static/css/not_found.nit" + system "curl -s {host}:{port}/not_found.nit" + + return null + end +end + +var app = new App +app.use("/", new StaticHandler("../examples/static_files/public/")) +app.use("/", new StaticHandler("../examples/static_files/files/")) +app.use("/static", new StaticHandler("../examples/static_files/public/")) +app.use("/static", new StaticHandler("../examples/static_files/files/")) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_router.nit b/lib/popcorn/tests/test_router.nit new file mode 100644 index 0000000..915ce4a --- /dev/null +++ b/lib/popcorn/tests/test_router.nit @@ -0,0 +1,73 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base_tests + +class TestHandler + super Handler + + var marker: String + + redef fun get(req, res) do res.send marker +end + +class HelloClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}" + system "curl -s {host}:{port}/" + system "curl -s {host}:{port}/user" + system "curl -s {host}:{port}/user/" + system "curl -s {host}:{port}/user/settings" + system "curl -s {host}:{port}/products" + system "curl -s {host}:{port}/products/" + system "curl -s {host}:{port}/products/list" + + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/user/not_found" + system "curl -s {host}:{port}/products/not_found" + return null + end +end + +var app = new App +app.use("/", new TestHandler("/")) +app.use("/about", new TestHandler("/about")) + +var router1 = new App +router1.use("/", new TestHandler("/user")) +router1.use("/settings", new TestHandler("/user/settings")) +app.use("/user", router1) + +var router2 = new App +router2.use("/", new TestHandler("/products")) +router2.use("/list", new TestHandler("/products/list")) +app.use("/products", router2) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new HelloClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/popcorn/tests/test_routes.nit b/lib/popcorn/tests/test_routes.nit new file mode 100644 index 0000000..2db660a --- /dev/null +++ b/lib/popcorn/tests/test_routes.nit @@ -0,0 +1,76 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base_tests + +class TestHandler + super Handler + + var marker: String + + redef fun get(req, res) do res.send marker +end + +class HelloClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}" + system "curl -s {host}:{port}/" + + system "curl -s {host}:{port}/misc" + system "curl -s {host}:{port}/misc/foo" + system "curl -s {host}:{port}/misc/foo/bar" + system "curl -s {host}:{port}/misc/foo/baz" + + system "curl -s {host}:{port}/user" + system "curl -s {host}:{port}/user/" + system "curl -s {host}:{port}/user/id" + system "curl -s {host}:{port}/user/id/profile" + system "curl -s {host}:{port}/user/id/misc/foo" + system "curl -s {host}:{port}/user/id/misc/foo/bar" + system "curl -s {host}:{port}/user/id/misc/foo/bar/baz" + + system "curl -s {host}:{port}/not_found" + system "curl -s {host}:{port}/user/id/not_found" + return null + end +end + +var app = new App +app.use("/", new TestHandler("/")) +app.use("/user", new TestHandler("/user")) +app.use("/misc/*", new TestHandler("/misc/everything")) +app.use("/user/:id", new TestHandler("/user/id")) +app.use("/user/:id/profile", new TestHandler("/user/id/profile")) +app.use("/user/:id/misc/*", new TestHandler("/user/id/misc/everything")) + +var host = test_host +var port = test_port + +# First, launch a server in the background +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +# Then, launch a client running test requests +var client = new HelloClient(host, port) +client.start +client.join +0.1.sleep + +# Force quit the server +exit 0 diff --git a/lib/popcorn/tests/tests.sh b/lib/popcorn/tests/tests.sh new file mode 100755 index 0000000..4406058 --- /dev/null +++ b/lib/popcorn/tests/tests.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa . +# +# 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" diff --git a/lib/postgresql/native_postgres.nit b/lib/postgresql/native_postgres.nit new file mode 100644 index 0000000..d32c908 --- /dev/null +++ b/lib/postgresql/native_postgres.nit @@ -0,0 +1,145 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2015-2016 Guilherme Mansur +# +# 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 +`} + +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 diff --git a/lib/postgresql/package.ini b/lib/postgresql/package.ini new file mode 100644 index 0000000..dc82d85 --- /dev/null +++ b/lib/postgresql/package.ini @@ -0,0 +1,11 @@ +[package] +name=postgresql +tags=database,lib +maintainer=Guilherme Mansur +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 diff --git a/lib/postgresql/postgres.nit b/lib/postgresql/postgres.nit new file mode 100644 index 0000000..22e498f --- /dev/null +++ b/lib/postgresql/postgres.nit @@ -0,0 +1,144 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Guilherme Mansur +# +# 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 diff --git a/lib/realtime.nit b/lib/realtime.nit index 8cfa4da..243b5d8 100644 --- a/lib/realtime.nit +++ b/lib/realtime.nit @@ -73,13 +73,16 @@ extern class Timespec `{struct timespec*`} 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. @@ -117,15 +120,33 @@ extern class Timespec `{struct timespec*`} 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; @@ -142,18 +163,43 @@ class Clock 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 diff --git a/lib/rubix.ini b/lib/rubix.ini new file mode 100644 index 0000000..c644c2e --- /dev/null +++ b/lib/rubix.ini @@ -0,0 +1,11 @@ +[package] +name=rubix +tags=algo,lib +maintainer=Lucas Bajolet +license=Apache-2.0 +[upstream] +browse=https://github.com/nitlang/nit/tree/master/lib/rubix.nit +git=https://github.com/nitlang/nit.git +git.directory=lib/rubix.nit +homepage=http://nitlanguage.org +issues=https://github.com/nitlang/nit/issues diff --git a/lib/rubix.nit b/lib/rubix.nit index 618a26f..8e63784 100644 --- a/lib/rubix.nit +++ b/lib/rubix.nit @@ -8,6 +8,20 @@ # You are allowed to redistribute it and sell it, alone or is a part of # another product. +# Rubix-cube modelization library +# +# As for now the library supports basic representation of a Rubix-cube +# The library is built for speed, most operations cost no allocations to perform. +# This does however mean that the library is NOT thread-safe and should be handled +# with the appropriate mutual-exclusion mechanisms to avoid memory corruption +# or unintended side-effects on a single cube. +# +# The library supports the singmaster notation as a way to perform operations +# on a Rubix cube. +# +# No solver is (yet) provided along with the library. +module rubix + import console private fun array1d_copy_to(fromarr: Array[Int], oarr: Array[Int]) do diff --git a/lib/serialization/caching.nit b/lib/serialization/caching.nit index 4541428..3554d22 100644 --- a/lib/serialization/caching.nit +++ b/lib/serialization/caching.nit @@ -55,7 +55,7 @@ end # then using a reference. class SerializerCache # Map of already serialized objects to the reference id - private var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int] + protected var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int] # Is `object` known? fun has_object(object: Serializable): Bool do return sent.keys.has(object) @@ -88,7 +88,7 @@ end # Used by `Deserializer` to find already deserialized objects by their reference. class DeserializerCache # Map of references to already deserialized objects. - private var received: Map[Int, Object] = new StrictHashMap[Int, Object] + protected var received: Map[Int, Object] = new StrictHashMap[Int, Object] # Is there an object associated to `id`? fun has_id(id: Int): Bool do return received.keys.has(id) diff --git a/lib/sqlite3/native_sqlite3.nit b/lib/sqlite3/native_sqlite3.nit index f441545..4b92786 100644 --- a/lib/sqlite3/native_sqlite3.nit +++ b/lib/sqlite3/native_sqlite3.nit @@ -22,9 +22,14 @@ in "C header" `{ #include `} +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`} @@ -75,11 +80,10 @@ 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 @@ -91,13 +95,8 @@ extern class NativeStatement `{sqlite3_stmt*`} 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` @@ -139,25 +138,18 @@ end 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 @@ -171,18 +163,18 @@ extern class NativeSqlite3 `{sqlite3 *`} `} # 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 `{ diff --git a/lib/sqlite3/sqlite3.nit b/lib/sqlite3/sqlite3.nit index 62fe8e5..322e7d7 100644 --- a/lib/sqlite3/sqlite3.nit +++ b/lib/sqlite3/sqlite3.nit @@ -57,8 +57,8 @@ class Sqlite3DB # 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 @@ -68,7 +68,7 @@ class Sqlite3DB # 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 @@ -99,7 +99,7 @@ class Sqlite3DB 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 @@ -182,7 +182,9 @@ class StatementEntry 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 @@ -305,8 +307,9 @@ redef universal Int super Sqlite3Data end 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 `'` # @@ -315,6 +318,24 @@ redef class String 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 diff --git a/lib/template/macro.nit b/lib/template/macro.nit index d0bc77d..615c45f 100644 --- a/lib/template/macro.nit +++ b/lib/template/macro.nit @@ -37,7 +37,7 @@ import template # A macro identifier is valid if: # # * starts with an uppercase letter -# * contains only numers, uppercase letters or '_' +# * contains only numbers, uppercase letters or '_' # # See `String::is_valid_macro_name` for more details. # diff --git a/misc/docker/Dockerfile b/misc/docker/Dockerfile new file mode 100644 index 0000000..95995bf --- /dev/null +++ b/misc/docker/Dockerfile @@ -0,0 +1,35 @@ +# This is a basic install of Nit on a debian base. + +FROM debian:jessie +MAINTAINER Jean Privat + +# 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 diff --git a/misc/docker/README.md b/misc/docker/README.md new file mode 100644 index 0000000..d255285 --- /dev/null +++ b/misc/docker/README.md @@ -0,0 +1,64 @@ +# Supported tags and respective Dockerfile links + +* [latest](https://github.com/nitlang/nit/blob/master/misc/docker/Dockerfile) +* [full](https://github.com/nitlang/nit/blob/master/misc/docker/full/Dockerfile) + +## What is Nit? + +Nit is an expressive language with a script-like syntax, a friendly type-system and aims at elegance, simplicity and intuitiveness. + +Nit has a simple straightforward style and can usually be picked up quickly, particularly by anyone who has programmed before. +While object-oriented, it allows procedural styles. + +More information on + +* Website +* Github +* Chatroom + +## 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 diff --git a/misc/docker/full/Dockerfile b/misc/docker/full/Dockerfile new file mode 100644 index 0000000..df0d214 --- /dev/null +++ b/misc/docker/full/Dockerfile @@ -0,0 +1,76 @@ +# This is a full install of Nit on a debian base. +# Full because most dependencies are installed so that most tests can be run + +FROM nitlang/nit:latest +MAINTAINER Jean Privat + +# Dependencies for more libs and tests +RUN dpkg --add-architecture i386 \ + && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + # Packages needed for lib/ + libcurl4-openssl-dev \ + libegl1-mesa-dev \ + libevent-dev \ + libgles1-mesa-dev \ + libgles2-mesa-dev \ + libgtk-3-dev \ + libncurses5-dev \ + libpq-dev \ + libsdl-image1.2-dev \ + libsdl-ttf2.0-dev \ + libsdl1.2-dev \ + libsdl2-dev \ + libsqlite3-dev \ + libx11-dev \ + libxdg-basedir-dev \ + postgresql \ + # Packages needed for contrib, platforms and FFI + ant \ + clang \ + default-jdk \ + file \ + inkscape \ + libopenmpi-dev \ + unzip \ + # Android + libc6:i386 \ + libstdc++6:i386 \ + zlib1g:i386 \ + # TODO neo4j emscripten test_glsl_validation + && rm -rf /var/lib/apt/lists/* + +# Install android sdk/ndk +RUN mkdir -p /opt \ + && cd /opt \ + # Android SDK + && curl https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz -o android-sdk-linux.tgz \ + && tar xzf android-sdk-linux.tgz \ + && rm android-sdk-linux.tgz \ + && echo y | android-sdk-linux/tools/android update sdk -a --no-ui --filter \ + # Hardcode minimal known working things + platform-tools,build-tools-22.0.1,android-22,android-10 \ + # Android NDK + && curl http://dl.google.com/android/repository/android-ndk-r11c-linux-x86_64.zip -o android-ndk.zip \ + && unzip -q android-ndk.zip \ + && ln -s android-ndk-r11c android-ndk \ + && rm android-ndk.zip \ + && printf "PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_NDK\nexport PATH\n" >> "/etc/profile.d/android.sh" + +# Setup environment variables + +ENV ANDROID_HOME /opt/android-sdk-linux +ENV ANDROID_NDK /opt/android-ndk +ENV PATH $PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_NDK + +# Run tests +RUN cd /root/nit/tests \ + # Basic tests + && ./testfull.sh || true \ + && rm -rf out/ alt/*.nit \ + # Nitunits + && ../bin/nitunit ../lib ../contrib || true \ + && rm -rf .nitunit \ + && ccache -C + +WORKDIR /root/nit +ENTRYPOINT [ "bash" ] diff --git a/misc/docker/hello/Dockerfile b/misc/docker/hello/Dockerfile new file mode 100644 index 0000000..c0a018b --- /dev/null +++ b/misc/docker/hello/Dockerfile @@ -0,0 +1,17 @@ +FROM nitlang/nit + +# Create a workdir +RUN mkdir -p /root/work +WORKDIR /root/work + +# Copy the source code in /root/work/ +COPY . /root/work/ + +# Compile +RUN nitc src/hello.nit --dir . \ + # Clear disk space + && ccache -C +# You can also use a Makefile or what you want + +# Say what to run +CMD ["./hello"] diff --git a/misc/docker/hello/src/hello.nit b/misc/docker/hello/src/hello.nit new file mode 100644 index 0000000..b732142 --- /dev/null +++ b/misc/docker/hello/src/hello.nit @@ -0,0 +1 @@ +print "hello" diff --git a/share/man/nitunit.md b/share/man/nitunit.md index e01dbd2..0576201 100644 --- a/share/man/nitunit.md +++ b/share/man/nitunit.md @@ -118,6 +118,15 @@ Finally, standard markdown documents can be checked with: $ 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. @@ -160,6 +169,34 @@ class TestFoo end ~~~~ +## Black Box Testing + +Sometimes, it is easier to validate a `TestCase` by comparing its output with a text file containing the expected result. + +For each TestCase `test_bar` of a TestSuite `test_mod.nit`, if the corresponding file `test_mod.sav/test_bar.res` exists, then the output of the test is compared with the file. + +The `diff(1)` command is used to perform the comparison. +The test is failed if non-zero is returned by `diff`. + +~~~ +module test_mod is test_suite +class TestFoo + fun test_bar do + print "Hello!" + end +end +~~~ + +Where `test_mod.sav/test_bar.res` contains + +~~~raw +Hello! +~~~ + +If no corresponding `.res` file exists, then the output of the TestCase is ignored. + +## Configuring TestSuites + `TestSuites` also provide methods to configure the test run: `before_test` and `after_test`: methods called before/after each test case. @@ -228,13 +265,19 @@ With the `--full` option, all imported modules (even those in standard) are also ### `-o`, `--output` Output name (default is 'nitunit.xml'). -### `nitunit` produces a XML file comatible with JUnit. +`nitunit` produces a XML file compatible with JUnit. ### `--dir` Working directory (default is '.nitunit'). In order to execute the tests, nit files are generated then compiled and executed in the giver working directory. +### `--nitc` +nitc compiler to use. + +By default, nitunit tries to locate the `nitc` program with the environment variable `NITC` or heuristics. +The option is used to indicate a specific nitc binary. + ### `--no-act` Does not compile and run tests. @@ -262,6 +305,13 @@ Also generate test case for private methods. ### `--only-show` Only display the skeleton, do not write any file. + +# ENVIRONMENT VARIABLES + +### `NITC` + +Indicate the specific Nit compiler executable to use. See `--nitc`. + # SEE ALSO The Nit language documentation and the source code of its tools and libraries may be downloaded from diff --git a/src/catalog.nit b/src/catalog.nit index 3ebc01e..1743179 100644 --- a/src/catalog.nit +++ b/src/catalog.nit @@ -229,6 +229,18 @@ class Catalog # 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] @@ -245,8 +257,13 @@ class Catalog do var p = persons.get_or_null(person) if p == null then - p = new Person.parse(person) - persons[person] = p + var new_p = new Person.parse(person) + # Maybe, we already have this person in fact? + p = persons.get_or_null(new_p.to_s) + if p == null then + p = new_p + persons[p.to_s] = p + end end var projs = contrib2proj[p] if not projs.has(mpackage) then @@ -338,9 +355,26 @@ class Catalog 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 @@ -348,9 +382,21 @@ class Catalog 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 @@ -361,11 +407,19 @@ class Catalog 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 diff --git a/src/interpreter/dynamic_loading_ffi/on_demand_compiler.nit b/src/interpreter/dynamic_loading_ffi/on_demand_compiler.nit index 4207a01..4e4a5b3 100644 --- a/src/interpreter/dynamic_loading_ffi/on_demand_compiler.nit +++ b/src/interpreter/dynamic_loading_ffi/on_demand_compiler.nit @@ -128,7 +128,12 @@ redef class AModule 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 diff --git a/src/loader.nit b/src/loader.nit index 6df4d61..967d335 100644 --- a/src/loader.nit +++ b/src/loader.nit @@ -381,9 +381,9 @@ redef class ModelBuilder if mgroup == null then # singleton package - var mpackage = new MPackage(pn, model) - mgroup = new MGroup(pn, mpackage, null) # same name for the root group - mgroup.filepath = path + var loc = new Location.opaque_file(path) + var mpackage = new MPackage(pn, model, loc) + mgroup = new MGroup(pn, loc, mpackage, null) # same name for the root group mpackage.root = mgroup toolcontext.info("found singleton package `{pn}` at {path}", 2) @@ -395,10 +395,8 @@ redef class ModelBuilder end end - var src = new SourceFile.from_string(path, "") - var loc = new Location(src, 0, 0, 0, 0) + var loc = new Location.opaque_file(path) var res = new MModule(model, mgroup, pn, loc) - res.filepath = path identified_modules_by_path[rp] = res identified_modules_by_path[path] = res @@ -483,17 +481,18 @@ redef class ModelBuilder end end + var loc = new Location.opaque_file(dirpath) var mgroup if parent == null then # no parent, thus new package if ini != null then pn = ini["package.name"] or else pn - var mpackage = new MPackage(pn, model) - mgroup = new MGroup(pn, mpackage, null) # same name for the root group + var mpackage = new MPackage(pn, model, loc) + mgroup = new MGroup(pn, loc, mpackage, null) # same name for the root group mpackage.root = mgroup toolcontext.info("found package `{mpackage}` at {dirpath}", 2) mpackage.ini = ini else - mgroup = new MGroup(pn, parent.mpackage, parent) + mgroup = new MGroup(pn, loc, parent.mpackage, parent) toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2) end @@ -507,7 +506,6 @@ redef class ModelBuilder mdoc.original_mentity = mgroup end - mgroup.filepath = dirpath mgroups[rdp] = mgroup return mgroup end @@ -612,6 +610,11 @@ redef class ModelBuilder 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 diff --git a/src/location.nit b/src/location.nit index 0c5b55d..99083ad 100644 --- a/src/location.nit +++ b/src/location.nit @@ -140,6 +140,15 @@ class Location end end + # Initialize a location corresponding to an opaque file. + # + # The path is used as is and is not open nor read. + init opaque_file(path: String) + do + var source = new SourceFile.from_string(path, "") + init(source, 0, 0, 0, 0) + end + # The index in the start character in the source fun pstart: Int do return file.line_starts[line_start-1] + column_start-1 diff --git a/src/model/mmodule.nit b/src/model/mmodule.nit index 8927f8c..1232352 100644 --- a/src/model/mmodule.nit +++ b/src/model/mmodule.nit @@ -17,7 +17,6 @@ # modules and module hierarchies in the metamodel module mmodule -import location import mpackage private import more_collections @@ -82,7 +81,13 @@ class MModule var mgroup: nullable MGroup # The path of the module source, if any - var filepath: nullable String = null is writable + # + # safe alias to `location.file.filepath` + fun filepath: nullable String do + var res = self.location.file + if res == null then return null + return res.filename + end # The package of the module if any # Safe alias for `mgroup.mpackage` @@ -95,8 +100,7 @@ class MModule # The short name of the module redef var name: String - # The origin of the definition - var location: Location is writable + redef var location: Location is writable # Alias for `name` redef fun to_s do return self.name diff --git a/src/model/model.nit b/src/model/model.nit index 6c8798b..9aefe19 100644 --- a/src/model/model.nit +++ b/src/model/model.nit @@ -285,8 +285,9 @@ redef class MModule if name == "Bool" and self.model.get_mclasses_by_name("Object") != null then # Bool is injected because it is needed by engine to code the result # of the implicit casts. - var c = new MClass(self, name, null, enum_kind, public_visibility) - var cladef = new MClassDef(self, c.mclass_type, new Location(null, 0,0,0,0)) + var loc = model.no_location + var c = new MClass(self, name, loc, null, enum_kind, public_visibility) + var cladef = new MClassDef(self, c.mclass_type, loc) cladef.set_supertypes([object_type]) cladef.add_in_hierarchy return c @@ -386,6 +387,8 @@ class MClass # In Nit, the name of a class cannot evolve in refinements redef var name + redef var location + # The canonical name of the class # # It is the name of the class prefixed by the full_name of the `intro_mmodule` @@ -588,8 +591,7 @@ class MClassDef # ENSURE: `bound_mtype.mclass == self.mclass` var bound_mtype: MClassType - # The origin of the definition - var location: Location + redef var location: Location # Internal name combining the module and the class # Example: "mymodule$MyClass" @@ -1165,6 +1167,8 @@ class MClassType redef fun model do return self.mclass.intro_mmodule.model + redef fun location do return mclass.location + # TODO: private init because strongly bounded to its mclass. see `mclass.mclass_type` # The formal arguments of the type @@ -1370,6 +1374,8 @@ class MVirtualType # Its the definitions of this property that determine the bound or the virtual type. var mproperty: MVirtualTypeProp + redef fun location do return mproperty.location + redef fun model do return self.mproperty.intro_mclassdef.mmodule.model redef fun lookup_bound(mmodule: MModule, resolved_receiver: MType): MType @@ -1500,6 +1506,8 @@ class MParameterType redef fun model do return self.mclass.intro_mmodule.model + redef fun location do return mclass.location + # The position of the parameter (0 for the first parameter) # FIXME: is `position` a better name? var rank: Int @@ -1625,6 +1633,8 @@ abstract class MProxyType # The base type var mtype: MType + redef fun location do return mtype.location + redef fun model do return self.mtype.model redef fun need_anchor do return mtype.need_anchor redef fun as_nullable do return mtype.as_nullable @@ -1951,6 +1961,8 @@ abstract class MProperty # The (short) name of the property redef var name + redef var location + # 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. @@ -2241,8 +2253,7 @@ abstract class MPropDef # The associated global property var mproperty: MPROPERTY - # The origin of the definition - var location: Location + redef var location: Location init do diff --git a/src/model/model_base.nit b/src/model/model_base.nit index 5cfe9b2..0c136b8 100644 --- a/src/model/model_base.nit +++ b/src/model/model_base.nit @@ -16,6 +16,7 @@ # The abstract concept of model and related common things module model_base +import location # The container class of a Nit object-oriented model. # A model knows modules, classes and properties and can retrieve them. @@ -23,6 +24,11 @@ class Model super MEntity redef fun model do return self + + # Place-holder object that means no-location + # + # See `MEntity::location` + var no_location = new Location(null, 0, 0, 0, 0) end # A named and possibly documented entity in the model. @@ -65,6 +71,16 @@ abstract class MEntity # indirect use should be restricted (e.g. to name a web-page) fun c_name: String is abstract + # The origin of the definition. + # + # Most model entities are defined in a specific place in the source base. + # + # Because most model entities have one, + # it is simpler for the client to have a non-nullable return value. + # For entities that lack a location, mock-up special locations are used instead. + # By default it is `model.no_location`. + fun location: Location do return model.no_location + # A Model Entity has a direct link to its model fun model: Model is abstract diff --git a/src/model/model_collect.nit b/src/model/model_collect.nit index 65c1c62..2590b0d 100644 --- a/src/model/model_collect.nit +++ b/src/model/model_collect.nit @@ -31,8 +31,38 @@ module model_collect import model_views +redef class MEntity + + # Collect modifier keywords like `redef`, `private` etc. + fun collect_modifiers: Array[String] do + return new Array[String] + end +end + +redef class MPackage + redef fun collect_modifiers do + var res = super + res.add "package" + return res + end +end + +redef class MGroup + redef fun collect_modifiers do + var res = super + res.add "group" + return res + end +end + redef class MModule + redef fun collect_modifiers do + var res = super + res.add "module" + return res + end + # Collect all transitive imports. fun collect_ancestors(view: ModelView): Set[MModule] do var res = new HashSet[MModule] @@ -133,6 +163,8 @@ end redef class MClass + redef fun collect_modifiers do return intro.collect_modifiers + # Collect direct parents of `self` with `visibility >= to min_visibility`. fun collect_parents(view: ModelView): Set[MClass] do var res = new HashSet[MClass] @@ -404,9 +436,8 @@ redef class MClassDef return res end - # Collect modifiers like redef, private etc. - fun collect_modifiers: Array[String] do - var res = new Array[String] + redef fun collect_modifiers do + var res = super if not is_intro then res.add "redef" else @@ -417,10 +448,13 @@ redef class MClassDef end end +redef class MProperty + redef fun collect_modifiers do return intro.collect_modifiers +end + redef class MPropDef - # Collect modifiers like redef, private, abstract, intern, fun etc. - fun collect_modifiers: Array[String] do - var res = new Array[String] + redef fun collect_modifiers do + var res = super if not is_intro then res.add "redef" else @@ -440,6 +474,8 @@ redef class MPropDef else res.add "fun" end + else if mprop isa MAttributeDef then + res.add "var" end return res end diff --git a/src/model/model_json.nit b/src/model/model_json.nit new file mode 100644 index 0000000..fc4b101 --- /dev/null +++ b/src/model/model_json.nit @@ -0,0 +1,304 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Make model entities Jsonable. +# +# To avoid cycles, every reference from a MEntity to another is replaced by a +# MEntityRef. +# +# How subobjects are retrieved using the MEntityRef is the responsability of the +# client. Json objects can be returned as this or inflated with concrete objet +# rather than the refs. +# +# TODO consider serialization module? +module model_json + +import model::model_collect +import json +import loader + +# A reference to another mentity. +class MEntityRef + super MEntity + + # MEntity to link to. + var mentity: MEntity + + # Return `self` as a Json Object. + # + # By default, MEntity references contain only the `full_name` of the Mentity. + # You should redefine this method in your client to implement a different behavior. + redef fun json do + var obj = new JsonObject + obj["full_name"] = mentity.full_name + return obj + end +end + +redef class MEntity + super Jsonable + + # Return `self` as a JsonObject. + # + # By default, every reference to another MEntity is replaced by a pointer + # to the MEntity::json_id. + fun json: JsonObject do + var obj = new JsonObject + obj["name"] = name + obj["class_name"] = class_name + obj["full_name"] = full_name + obj["mdoc"] = mdoc_or_fallback + var modifiers = new JsonArray + for modifier in collect_modifiers do + modifiers.add modifier + end + obj["modifiers"] = modifiers + return obj + end + + redef fun to_json do return json.to_json +end + +redef class MDoc + super Jsonable + + # Return `self` as a JsonObject. + fun json: JsonObject do + var obj = new JsonObject + obj["content"] = content.join("\n") + obj["location"] = location + return obj + end + + redef fun to_json do return json.to_json +end + +redef class Location + super Jsonable + + # Return `self` as a JsonObject. + fun json: JsonObject do + var obj = new JsonObject + obj["column_end"] = column_end + obj["column_start"] = column_start + obj["line_end"] = line_end + obj["line_start"] = line_start + var file = self.file + if file != null then + obj["file"] = file.filename + end + return obj + end + + redef fun to_json do return json.to_json +end + +redef class MVisibility + super Jsonable + + redef fun to_json do return to_s.to_json +end + +redef class MPackage + + redef fun json do + var obj = super + obj["visibility"] = public_visibility + if ini != null then + obj["ini"] = new JsonObject.from(ini.as(not null).to_map) + end + obj["root"] = to_mentity_ref(root) + obj["mgroups"] = to_mentity_refs(mgroups) + return obj + end +end + +redef class MGroup + redef fun json do + var obj = super + obj["visibility"] = public_visibility + obj["is_root"] = is_root + obj["mpackage"] = to_mentity_ref(mpackage) + obj["default_mmodule"] = to_mentity_ref(default_mmodule) + obj["parent"] = to_mentity_ref(parent) + obj["mmodules"] = to_mentity_refs(mmodules) + obj["mgroups"] = to_mentity_refs(in_nesting.direct_smallers) + return obj + end +end + +redef class MModule + redef fun json do + var obj = super + obj["location"] = location + obj["visibility"] = public_visibility + obj["mpackage"] = to_mentity_ref(mpackage) + obj["mgroup"] = to_mentity_ref(mgroup) + obj["intro_mclasses"] = to_mentity_refs(intro_mclasses) + obj["mclassdefs"] = to_mentity_refs(mclassdefs) + return obj + end +end + +redef class MClass + redef fun json do + var obj = super + obj["visibility"] = visibility + var arr = new JsonArray + for mparameter in mparameters do arr.add mparameter + obj["mparameters"] = arr + obj["intro"] = to_mentity_ref(intro) + obj["intro_mmodule"] = to_mentity_ref(intro_mmodule) + obj["mpackage"] = to_mentity_ref(intro_mmodule.mpackage) + obj["mclassdefs"] = to_mentity_refs(mclassdefs) + return obj + end +end + +redef class MClassDef + redef fun json do + var obj = super + obj["visibility"] = mclass.visibility + obj["location"] = location + obj["is_intro"] = is_intro + var arr = new JsonArray + for mparameter in mclass.mparameters do arr.add mparameter + obj["mparameters"] = arr + obj["mmodule"] = to_mentity_ref(mmodule) + obj["mclass"] = to_mentity_ref(mclass) + obj["mpropdefs"] = to_mentity_refs(mpropdefs) + obj["intro_mproperties"] = to_mentity_refs(intro_mproperties) + return obj + end +end + +redef class MProperty + redef fun json do + var obj = super + obj["visibility"] = visibility + obj["intro"] = to_mentity_ref(intro) + obj["intro_mclassdef"] = to_mentity_ref(intro_mclassdef) + obj["mpropdefs"] = to_mentity_refs(mpropdefs) + return obj + end +end + +redef class MMethod + redef fun json do + var obj = super + obj["is_init"] = is_init + obj["msignature"] = intro.msignature + return obj + end +end + +redef class MAttribute + redef fun json do + var obj = super + obj["static_mtype"] = to_mentity_ref(intro.static_mtype) + return obj + end +end + +redef class MVirtualTypeProp + redef fun json do + var obj = super + obj["mvirtualtype"] = to_mentity_ref(mvirtualtype) + obj["bound"] = to_mentity_ref(intro.bound) + return obj + end +end + +redef class MPropDef + redef fun json do + var obj = super + obj["visibility"] = mproperty.visibility + obj["location"] = location + obj["is_intro"] = is_intro + obj["mclassdef"] = to_mentity_ref(mclassdef) + obj["mproperty"] = to_mentity_ref(mproperty) + return obj + end +end + +redef class MMethodDef + redef fun json do + var obj = super + obj["msignature"] = msignature + return obj + end +end + +redef class MAttributeDef + redef fun json do + var obj = super + obj["static_mtype"] = to_mentity_ref(static_mtype) + return obj + end +end + +redef class MVirtualTypeDef + redef fun json do + var obj = super + obj["bound"] = to_mentity_ref(bound) + obj["is_fixed"] = is_fixed + return obj + end +end + +redef class MSignature + redef fun json do + var obj = new JsonObject + obj["arity"] = arity + var arr = new JsonArray + for mparam in mparameters do arr.add mparam + obj["mparams"] = arr + obj["return_mtype"] = to_mentity_ref(return_mtype) + obj["vararg_rank"] = vararg_rank + return obj + end +end + +redef class MParameterType + redef fun json do + var obj = new JsonObject + obj["name"] = name + obj["rank"] = rank + obj["mtype"] = to_mentity_ref(mclass.intro.bound_mtype.arguments[rank]) + return obj + end +end + +redef class MParameter + redef fun json do + var obj = new JsonObject + obj["is_vararg"] = is_vararg + obj["name"] = name + obj["mtype"] = to_mentity_ref(mtype) + return obj + end +end + +# Create a ref to a `mentity`. +fun to_mentity_ref(mentity: nullable MEntity): nullable MEntityRef do + if mentity == null then return null + return new MEntityRef(mentity) +end + +# Return a collection of `mentities` as a JsonArray of MEntityRefs. +fun to_mentity_refs(mentities: Collection[MEntity]): JsonArray do + var array = new JsonArray + for mentity in mentities do array.add to_mentity_ref(mentity) + return array +end diff --git a/src/model/model_views.nit b/src/model/model_views.nit index f130f09..566a5bd 100644 --- a/src/model/model_views.nit +++ b/src/model/model_views.nit @@ -132,6 +132,14 @@ class ModelView return res end + # Searches the MEntity that matches `full_name`. + fun mentity_by_full_name(full_name: String): nullable MEntity do + for mentity in mentities do + if mentity.full_name == full_name then return mentity + end + return null + end + # Looks up a MEntity by its full `namespace`. # # Usefull when `mentities_by_name` returns conflicts. diff --git a/src/model/mpackage.nit b/src/model/mpackage.nit index 1771938..8309b5a 100644 --- a/src/model/mpackage.nit +++ b/src/model/mpackage.nit @@ -34,6 +34,8 @@ class MPackage # The model of the package redef var model: Model + redef var location + # The root of the group tree var root: nullable MGroup = null is writable @@ -66,6 +68,8 @@ class MGroup # empty name for a default group in a single-module package redef var name: String + redef var location + # The enclosing package var mpackage: MPackage @@ -94,7 +98,14 @@ class MGroup fun is_root: Bool do return mpackage.root == self # The filepath (usually a directory) of the group, if any - var filepath: nullable String = null is writable + # + # safe alias to `location.file.filename` + fun filepath: nullable String do + var res + res = self.location.file + if res == null then return null + return res.filename + end init do diff --git a/src/modelize/modelize_class.nit b/src/modelize/modelize_class.nit index e020b50..f5ed779 100644 --- a/src/modelize/modelize_class.nit +++ b/src/modelize/modelize_class.nit @@ -122,7 +122,7 @@ redef class ModelBuilder end end - mclass = new MClass(mmodule, name, names, mkind, mvisibility) + mclass = new MClass(mmodule, name, nclassdef.location, names, mkind, mvisibility) #print "new class {mclass}" else if nclassdef isa AStdClassdef and nmodule.mclass2nclassdef.has_key(mclass) then error(nclassdef, "Error: a class `{name}` is already defined at line {nmodule.mclass2nclassdef[mclass].location.line_start}.") diff --git a/src/modelize/modelize_property.nit b/src/modelize/modelize_property.nit index 141ed81..246e813 100644 --- a/src/modelize/modelize_property.nit +++ b/src/modelize/modelize_property.nit @@ -145,7 +145,7 @@ redef class ModelBuilder # Look for the init in Object, or create it if mclassdef.mclass.name == "Object" and the_root_init_mmethod == null then # Create the implicit root-init method - var mprop = new MMethod(mclassdef, "init", mclassdef.mclass.visibility) + var mprop = new MMethod(mclassdef, "init", nclassdef.location, mclassdef.mclass.visibility) mprop.is_root_init = true var mpropdef = new MMethodDef(mclassdef, mprop, nclassdef.location) var mparameters = new Array[MParameter] @@ -822,7 +822,7 @@ redef class AMethPropdef end if mprop == null then var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility) - mprop = new MMethod(mclassdef, name, mvisibility) + mprop = new MMethod(mclassdef, name, self.location, mvisibility) if look_like_a_root_init and modelbuilder.the_root_init_mmethod == null then modelbuilder.the_root_init_mmethod = mprop mprop.is_root_init = true @@ -836,9 +836,7 @@ redef class AMethPropdef return end else - if mprop.is_broken then - return - end + if mprop.is_broken then return if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, not self isa AMainMethPropdef, mprop) then return check_redef_property_visibility(modelbuilder, self.n_visibility, mprop) end @@ -1186,7 +1184,7 @@ redef class AAttrPropdef modelbuilder.error(self, "Error: attempt to define attribute `{name}` in the {mclass.kind} `{mclass}`.") end - var mprop = new MAttribute(mclassdef, "_" + name, private_visibility) + var mprop = new MAttribute(mclassdef, "_" + name, self.location, private_visibility) var mpropdef = new MAttributeDef(mclassdef, mprop, self.location) self.mpropdef = mpropdef modelbuilder.mpropdef2npropdef[mpropdef] = self @@ -1196,9 +1194,13 @@ redef class AAttrPropdef var mreadprop = modelbuilder.try_get_mproperty_by_name(nid2, mclassdef, readname).as(nullable MMethod) if mreadprop == null then var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility) - mreadprop = new MMethod(mclassdef, readname, mvisibility) - if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then return + mreadprop = new MMethod(mclassdef, readname, self.location, mvisibility) + 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 @@ -1248,7 +1250,7 @@ redef class AAttrPropdef return end is_lazy = true - var mlazyprop = new MAttribute(mclassdef, "lazy _" + name, none_visibility) + var mlazyprop = new MAttribute(mclassdef, "lazy _" + name, self.location, none_visibility) mlazyprop.is_fictive = true var mlazypropdef = new MAttributeDef(mclassdef, mlazyprop, self.location) mlazypropdef.is_fictive = true @@ -1295,10 +1297,14 @@ redef class AAttrPropdef # By default, use protected visibility at most if mvisibility > protected_visibility then mvisibility = protected_visibility end - mwriteprop = new MMethod(mclassdef, writename, mvisibility) - if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then return + mwriteprop = new MMethod(mclassdef, writename, self.location, mvisibility) + 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) @@ -1602,12 +1608,13 @@ redef class ATypePropdef var mprop = modelbuilder.try_get_mproperty_by_name(self.n_qid, mclassdef, name) if mprop == null then var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility) - mprop = new MVirtualTypeProp(mclassdef, name, mvisibility) + mprop = new MVirtualTypeProp(mclassdef, name, self.location, mvisibility) for c in name.chars do if c >= 'a' and c<= 'z' then modelbuilder.warning(n_qid, "bad-type-name", "Warning: lowercase in the virtual type `{name}`.") break end else + if mprop.is_broken then return assert mprop isa MVirtualTypeProp check_redef_property_visibility(modelbuilder, self.n_visibility, mprop) end diff --git a/src/neo.nit b/src/neo.nit index c110468..1e3fb5d 100644 --- a/src/neo.nit +++ b/src/neo.nit @@ -365,9 +365,14 @@ class NeoModel node.labels.add "MEntity" node.labels.add model_name node["name"] = mentity.name - if mentity.mdoc != null then - node["mdoc"] = new JsonArray.from(mentity.mdoc.content) - node["mdoc_location"] = mentity.mdoc.location.to_s + if not mentity isa MSignature then + #FIXME: MSignature is a MEntity, but has no model :/ + node["location"] = mentity.location.to_s + end + var mdoc = mentity.mdoc + if mdoc != null then + node["mdoc"] = new JsonArray.from(mdoc.content) + node["mdoc_location"] = mdoc.location.to_s end return node end @@ -391,7 +396,8 @@ class NeoModel if m isa MPackage then return m assert node.labels.has("MPackage") - var mpackage = new MPackage(node["name"].to_s, model) + var location = to_location(node["location"].to_s) + var mpackage = new MPackage(node["name"].to_s, model, location) mentities[node.id.as(Int)] = mpackage set_doc(node, mpackage) mpackage.root = to_mgroup(model, node.out_nodes("ROOT").first) @@ -424,13 +430,14 @@ class NeoModel if m isa MGroup then return m assert node.labels.has("MGroup") + var location = to_location(node["location"].to_s) var mpackage = to_mpackage(model, node.out_nodes("PROJECT").first) var parent: nullable MGroup = null var out = node.out_nodes("PARENT") if not out.is_empty then parent = to_mgroup(model, out.first) end - var mgroup = new MGroup(node["name"].to_s, mpackage, parent) + var mgroup = new MGroup(node["name"].to_s, location, mpackage, parent) mentities[node.id.as(Int)] = mgroup set_doc(node, mgroup) return mgroup @@ -440,7 +447,6 @@ class NeoModel private fun mmodule_node(mmodule: MModule): NeoNode do var node = make_node(mmodule) node.labels.add "MModule" - node["location"] = mmodule.location.to_s for parent in mmodule.in_importation.direct_greaters do node.out_edges.add(new NeoEdge(node, "IMPORTS", to_node(parent))) end @@ -504,6 +510,7 @@ class NeoModel assert node.labels.has("MClass") var mmodule = to_mmodule(model, node.in_nodes("INTRODUCES").first) var name = node["name"].to_s + var location = to_location(node["location"].to_s) var kind = to_kind(node["kind"].to_s) var visibility = to_visibility(node["visibility"].to_s) var parameter_names = new Array[String] @@ -512,7 +519,7 @@ class NeoModel parameter_names.add e.to_s end end - var mclass = new MClass(mmodule, name, parameter_names, kind, visibility) + var mclass = new MClass(mmodule, name, location, parameter_names, kind, visibility) mentities[node.id.as(Int)] = mclass set_doc(node, mclass) return mclass @@ -522,7 +529,6 @@ class NeoModel private fun mclassdef_node(mclassdef: MClassDef): NeoNode do var node = make_node(mclassdef) node.labels.add "MClassDef" - node["location"] = mclassdef.location.to_s node.out_edges.add(new NeoEdge(node, "BOUNDTYPE", to_node(mclassdef.bound_mtype))) node.out_edges.add(new NeoEdge(node, "MCLASS", to_node(mclassdef.mclass))) for mproperty in mclassdef.intro_mproperties do @@ -590,18 +596,19 @@ class NeoModel assert node.labels.has("MProperty") var intro_mclassdef = to_mclassdef(model, node.out_nodes("INTRO_CLASSDEF").first) var name = node["name"].to_s + var location = to_location(node["location"].to_s) var visibility = to_visibility(node["visibility"].to_s) var mprop: nullable MProperty = null if node.labels.has("MMethod") then - mprop = new MMethod(intro_mclassdef, name, visibility) + mprop = new MMethod(intro_mclassdef, name, location, visibility) mprop.is_init = node["is_init"].as(Bool) else if node.labels.has("MAttribute") then - mprop = new MAttribute(intro_mclassdef, name, visibility) + mprop = new MAttribute(intro_mclassdef, name, location, visibility) else if node.labels.has("MVirtualTypeProp") then - mprop = new MVirtualTypeProp(intro_mclassdef, name, visibility) + mprop = new MVirtualTypeProp(intro_mclassdef, name, location, visibility) else if node.labels.has("MInnerClass") then var inner = to_mclass(model, node.out_nodes("NESTS").first) - mprop = new MInnerClass(intro_mclassdef, name, visibility, inner) + mprop = new MInnerClass(intro_mclassdef, name, location, visibility, inner) end if mprop == null then print "not yet implemented to_mproperty for {node.labels.join(",")}" @@ -616,7 +623,6 @@ class NeoModel private fun mpropdef_node(mpropdef: MPropDef): NeoNode do var node = make_node(mpropdef) node.labels.add "MPropDef" - node["location"] = mpropdef.location.to_s node.out_edges.add(new NeoEdge(node, "DEFINES", to_node(mpropdef.mproperty))) if mpropdef isa MMethodDef then node.labels.add "MMethodDef" diff --git a/src/nitcatalog.nit b/src/nitcatalog.nit index b3dcd32..5c478f0 100644 --- a/src/nitcatalog.nit +++ b/src/nitcatalog.nit @@ -315,6 +315,15 @@ redef class Catalog end res.add "\n" + res.add "

Quality

\n
    \n" + var errors = errors[mpackage] + if errors > 0 then + res.add "
  • {errors} errors
  • \n" + end + res.add "
  • {warnings[mpackage]} warnings ({warnings_per_kloc[mpackage]}/kloc)
  • \n" + res.add "
  • {documentation_score[mpackage]}% documented
  • \n" + res.add "
\n" + res.add "

Tags

\n" var ts2 = new Array[String] for t in mpackage.tags do @@ -474,6 +483,10 @@ redef class Catalog res.add "methods\n" res.add "lines\n" res.add "score\n" + res.add "errors\n" + res.add "warnings\n" + res.add "w/kloc\n" + res.add "doc\n" res.add "" for p in mpackages do res.add "" @@ -493,6 +506,10 @@ redef class Catalog res.add "{mmethods[p]}" res.add "{loc[p]}" res.add "{score[p]}" + res.add "{errors[p]}" + res.add "{warnings[p]}" + res.add "{warnings_per_kloc[p]}" + res.add "{documentation_score[p]}" res.add "\n" end res.add "\n" diff --git a/src/nitlight.nit b/src/nitlight.nit index 165ea13..176ec5f 100644 --- a/src/nitlight.nit +++ b/src/nitlight.nit @@ -41,7 +41,7 @@ var args = toolcontext.option_context.rest 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 diff --git a/src/nitunit.nit b/src/nitunit.nit index 522591d..0d89dc6 100644 --- a/src/nitunit.nit +++ b/src/nitunit.nit @@ -20,7 +20,7 @@ import testing var toolcontext = new ToolContext -toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show) +toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show, toolcontext.opt_nitc) toolcontext.tooldescription = "Usage: nitunit [OPTION]... ...\nExecutes the unit tests from Nit source files." toolcontext.process_options(args) @@ -63,6 +63,12 @@ if toolcontext.opt_gen_unit.value then exit(0) end +"NIT_TESTING".setenv("true") + +var test_dir = toolcontext.test_dir +test_dir.mkdir +"# This file prevents the Nit modules of the directory to be part of the package".write_to_file(test_dir / "packages.ini") + var page = new HTMLTag("testsuites") if toolcontext.opt_full.value then mmodules = model.mmodules diff --git a/src/nitweb.nit b/src/nitweb.nit index b924ec2..c055ee9 100644 --- a/src/nitweb.nit +++ b/src/nitweb.nit @@ -47,15 +47,15 @@ private class NitwebPhase var host = toolcontext.opt_host.value or else "localhost" var port = toolcontext.opt_port.value - var srv = new NitServer(host, port.to_i) - srv.routes.add new Route("/random", new RandomAction(srv, model)) - srv.routes.add new Route("/doc/:namespace", new DocAction(srv, model, modelbuilder)) - srv.routes.add new Route("/code/:namespace", new CodeAction(srv, model, modelbuilder)) - srv.routes.add new Route("/search/:namespace", new SearchAction(srv, model)) - srv.routes.add new Route("/uml/:namespace", new UMLDiagramAction(srv, model, mainmodule)) - srv.routes.add new Route("/", new TreeAction(srv, model)) + var app = new App + app.use("/random", new RandomAction(model)) + app.use("/doc/:namespace", new DocAction(model, modelbuilder)) + app.use("/code/:namespace", new CodeAction(model, modelbuilder)) + app.use("/search/:namespace", new SearchAction(model)) + app.use("/uml/:namespace", new UMLDiagramAction(model, mainmodule)) + app.use("/", new TreeAction(model)) - srv.listen + app.listen(host, port.to_i) end end diff --git a/src/parser/README.md b/src/parser/README.md index 64ceecf..73ebcd0 100644 --- a/src/parser/README.md +++ b/src/parser/README.md @@ -1,6 +1,6 @@ Parser and AST for the Nit language -The parser ans the AST are mostly used by all tools. +The parser and the AST are mostly used by all tools. The `parser` is the tool that transform source-files into abstract syntax trees (AST) (see `parser_nodes`) While the AST is a higher abstraction than blob of text, the AST is still limited and represents only *What the programmer says*. diff --git a/src/platform/app_annotations.nit b/src/platform/app_annotations.nit index 62fbbb0..1d60f15 100644 --- a/src/platform/app_annotations.nit +++ b/src/platform/app_annotations.nit @@ -88,8 +88,6 @@ redef class AAnnotation return "" else for arg in args do - var format_error = "Syntax Eror: `{name}` expects its arguments to be of type Int or a call to `git_revision`." - var value value = arg.as_int if value != null then @@ -107,7 +105,13 @@ redef class AAnnotation # Get Git short revision var proc = new ProcessReader("git", "rev-parse", "--short", "HEAD") proc.wait - assert proc.status == 0 + if proc.status != 0 then + # Fallback if this is not a git repository or git bins are missing + version_fields.add "0" + modelbuilder.warning(self, "git_revision", "Warning: `git_revision` used outside of a git repository or git binaries not available") + continue + end + var lines = proc.read_all var revision = lines.split("\n").first @@ -122,6 +126,7 @@ redef class AAnnotation continue end + var format_error = "Syntax Error: `{name}` expects its arguments to be of type Int or a call to `git_revision`." modelbuilder.error(self, format_error) return "" end diff --git a/src/semantize/typing.nit b/src/semantize/typing.nit index 69329f9..6c4de01 100644 --- a/src/semantize/typing.nit +++ b/src/semantize/typing.nit @@ -637,8 +637,7 @@ end class CallSite super MEntity - # The associated location of the callsite - var location: Location + redef var location: Location # The static type of the receiver (possibly unresolved) var recv: MType diff --git a/src/test_model_visitor.nit b/src/test_model_visitor.nit index b46af5c..d7cc947 100644 --- a/src/test_model_visitor.nit +++ b/src/test_model_visitor.nit @@ -117,6 +117,6 @@ do var c = "" var d = e.mdoc_or_fallback if d != null and d.content.not_empty then c = d.content.first - print "{n}\t{e.class_name}\t{c}" + print "{n}\t{e.class_name}\t{e.location}\t{c}" end end diff --git a/src/testing/testing_base.nit b/src/testing/testing_base.nit index 62cfb08..4249df5 100644 --- a/src/testing/testing_base.nit +++ b/src/testing/testing_base.nit @@ -17,6 +17,7 @@ module testing_base import modelize private import parser_util +import html redef class ToolContext # opt --full @@ -27,6 +28,8 @@ redef class ToolContext var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir") # opt --no-act var opt_noact = new OptionBool("Does not compile and run tests", "--no-act") + # opt --nitc + var opt_nitc = new OptionString("nitc compiler to use", "--nitc") # Working directory for testing. fun test_dir: String do @@ -34,4 +37,137 @@ redef class ToolContext if dir == null then return ".nitunit" return dir end + + # Search the `nitc` compiler to use + # + # If not `nitc` is suitable, then prints an error and quit. + fun find_nitc: String + do + var nitc = opt_nitc.value + if nitc != null then + if not nitc.file_exists then + fatal_error(null, "error: cannot find `{nitc}` given by --nitc.") + abort + end + return nitc + end + + nitc = "NITC".environ + if nitc != "" then + if not nitc.file_exists then + fatal_error(null, "error: cannot find `{nitc}` given by NITC.") + abort + end + return nitc + end + + var nit_dir = nit_dir + nitc = nit_dir/"bin/nitc" + if not nitc.file_exists then + fatal_error(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.") + abort + end + return nitc + end + + # Execute a system command in a more safe context than `Sys::system`. + fun safe_exec(command: String): Int + do + info(command, 2) + var real_command = """ +bash -c " +ulimit -f {{{ulimit_file}}} 2> /dev/null +ulimit -t {{{ulimit_usertime}}} 2> /dev/null +{{{command}}} +" +""" + return system(real_command) + end + + # The maximum size (in KB) of files written by a command executed trough `safe_exec` + # + # Default: 64MB + var ulimit_file = 65536 is writable + + # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec` + # + # Default: 10 CPU minute + var ulimit_usertime = 600 is writable +end + +# A unit test is an elementary test discovered, run and reported bu nitunit. +# +# This class factorizes `DocUnit` and `TestCase`. +abstract class UnitTest + + # Error occurred during test-case execution. + var error: nullable String = null is writable + + # Was the test case executed at least once? + var was_exec = false is writable + + # Return the `TestCase` in XML format compatible with Jenkins. + # + # See to_xml + fun to_xml: HTMLTag do + var tc = new HTMLTag("testcase") + tc.attr("classname", xml_classname) + tc.attr("name", xml_name) + var error = self.error + if error != null then + if was_exec then + tc.open("error").append("Runtime Error") + else + tc.open("failure").append("Compilation Error") + end + tc.open("system-err").append(error.trunc(8192).filter_nonprintable) + end + return tc + end + + # The `classname` attribute of the XML format. + # + # NOTE: jenkins expects a '.' in the classname attr + fun xml_classname: String is abstract + + # The `name` attribute of the XML format. + # + # See to_xml + fun xml_name: String is abstract +end + +redef class String + # If needed, truncate `self` at `max_length` characters and append an informative `message`. + # + # ~~~ + # assert "hello".trunc(10) == "hello" + # assert "hello".trunc(2) == "he[truncated. Full size is 5]" + # assert "hello".trunc(2, "...") == "he..." + # ~~~ + fun trunc(max_length: Int, message: nullable String): String + do + if length <= max_length then return self + if message == null then message = "[truncated. Full size is {length}]" + return substring(0, max_length) + message + end + + # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space). + # + # ~~~ + # assert "hello".filter_nonprintable == "hello" + # assert "\r\n\t".filter_nonprintable == "^13\n^9" + # ~~~ + fun filter_nonprintable: String + do + var buf = new Buffer + for c in self do + var cp = c.code_point + if cp < 32 and c != '\n' then + buf.append "^{cp}" + else + buf.add c + end + end + return buf.to_s + end end diff --git a/src/testing/testing_doc.nit b/src/testing/testing_doc.nit index 2c0e456..1ea2cf7 100644 --- a/src/testing/testing_doc.nit +++ b/src/testing/testing_doc.nit @@ -36,12 +36,6 @@ class NitUnitExecutor # The XML node associated to the module var testsuite: HTMLTag - # All blocks of code from a same `ADoc` - var blocks = new Array[Buffer] - - # All failures from a same `ADoc` - var failures = new Array[String] - # Markdown processor used to parse markdown comments and extract code. var mdproc = new MarkdownProcessor @@ -55,14 +49,22 @@ class NitUnitExecutor # used to generate distinct names var cpt = 0 + # The last docunit extracted from a mdoc. + # + # Is used because a new code-block might just be added to it. + var last_docunit: nullable DocUnit = null + + var xml_classname: String is noautoinit + + var xml_name: String is noautoinit + # The entry point for a new `ndoc` node # Fill `docunits` with new discovered unit of tests. - # - # `tc` (testcase) is the pre-filled XML node - fun extract(mdoc: MDoc, tc: HTMLTag) + fun extract(mdoc: MDoc, xml_classname, xml_name: String) do - blocks.clear - failures.clear + last_docunit = null + self.xml_classname = xml_classname + self.xml_name = xml_name self.mdoc = mdoc @@ -70,22 +72,6 @@ class NitUnitExecutor mdproc.process(mdoc.content.join("\n")) toolcontext.check_errors - - if not failures.is_empty then - for msg in failures do - var ne = new HTMLTag("failure") - ne.attr("message", msg) - tc.add ne - toolcontext.modelbuilder.unit_entities += 1 - toolcontext.modelbuilder.failed_entities += 1 - end - if blocks.is_empty then testsuite.add(tc) - end - - if blocks.is_empty then return - for block in blocks do - docunits.add new DocUnit(mdoc, tc, block.write_to_string) - end end # All extracted docunits @@ -96,6 +82,9 @@ class NitUnitExecutor do var simple_du = new Array[DocUnit] for du in docunits do + # Skip existing errors + if du.error != null then continue + var ast = toolcontext.parse_something(du.block) if ast isa AExpr then simple_du.add du @@ -105,6 +94,10 @@ class NitUnitExecutor end test_simple_docunits(simple_du) + + for du in docunits do + testsuite.add du.to_xml + end end # Executes multiples doc-units in a shared program. @@ -128,7 +121,7 @@ class NitUnitExecutor i += 1 f.write("fun run_{i} do\n") - f.write("# {du.testcase.attrs["name"]}\n") + f.write("# {du.full_name}\n") f.write(du.block) f.write("end\n") end @@ -153,34 +146,19 @@ class NitUnitExecutor i = 0 for du in dus do - var tc = du.testcase - toolcontext.modelbuilder.unit_entities += 1 i += 1 - toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1) - var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 '{file}.out1' 2>&1 >'{file}.out1' 2>&1 '{file}.out1' 2>&1 '{file}.out1' 2>&1 ") - tc.attr("name", "") - d2m.extract(ndoc.to_mdoc, tc) + d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + ".", "") end label x for nclassdef in nmodule.n_classdefs do var mclassdef = nclassdef.mclassdef @@ -393,10 +462,7 @@ redef class ModelBuilder var ndoc = nclassdef.n_doc if ndoc != null then doc_entities += 1 - tc = new HTMLTag("testcase") - tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name) - tc.attr("name", "") - d2m.extract(ndoc.to_mdoc, tc) + d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, "") end end for npropdef in nclassdef.n_propdefs do @@ -406,10 +472,7 @@ redef class ModelBuilder var ndoc = npropdef.n_doc if ndoc != null then doc_entities += 1 - tc = new HTMLTag("testcase") - tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name) - tc.attr("name", mpropdef.mproperty.full_name) - d2m.extract(ndoc.to_mdoc, tc) + d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, mpropdef.mproperty.full_name) end end end @@ -435,18 +498,13 @@ redef class ModelBuilder prefix = prefix.join_path(mgroup.to_s) var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts) - var tc - total_entities += 1 var mdoc = mgroup.mdoc if mdoc == null then return ts doc_entities += 1 - tc = new HTMLTag("testcase") # NOTE: jenkins expects a '.' in the classname attr - tc.attr("classname", "nitunit." + mgroup.full_name) - tc.attr("name", "") - d2m.extract(mdoc, tc) + d2m.extract(mdoc, "nitunit." + mgroup.full_name, "") d2m.run_tests @@ -466,17 +524,11 @@ redef class ModelBuilder var prefix = toolcontext.test_dir / "file" var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts) - var tc - total_entities += 1 doc_entities += 1 - tc = new HTMLTag("testcase") # NOTE: jenkins expects a '.' in the classname attr - tc.attr("classname", "nitunit.") - tc.attr("name", file) - - d2m.extract(mdoc, tc) + d2m.extract(mdoc, "nitunit.", file) d2m.run_tests return ts diff --git a/src/testing/testing_suite.nit b/src/testing/testing_suite.nit index 0de6e0a..ee2701f 100644 --- a/src/testing/testing_suite.nit +++ b/src/testing/testing_suite.nit @@ -183,12 +183,7 @@ class TestSuite # Compile all `test_cases` cases in one file. fun compile do # find nitc - var nit_dir = toolcontext.nit_dir - var nitc = nit_dir/"bin/nitc" - if not nitc.file_exists then - toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.") - toolcontext.check_errors - end + var nitc = toolcontext.find_nitc # compile test suite var file = test_file var module_file = mmodule.location.file @@ -199,7 +194,7 @@ class TestSuite end var include_dir = module_file.filename.dirname var cmd = "{nitc} --no-color '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 '{res_name}.out1' 2>&1 '{res_name}.out1' 2>&1 '{res_name}.diff' 2>&1 Error {code}" - tpl.add "
{message.html_escape}
" - response.body = tpl.write_to_string - return response - end - - # Render a view as a HttpResponse 200. - fun render_view(view: NitView): HttpResponse do - var response = new HttpResponse(200) - response.body = view.render(srv).write_to_string - return response - end - - # Return a HttpResponse containing `json`. - fun render_json(json: Jsonable): HttpResponse do - var response = new HttpResponse(200) - response.body = json.to_json - return response - end -end +import model::model_json +import popcorn # Specific nitcorn Action that uses a Model class ModelAction - super NitAction + super Handler # Model to use. var model: Model + # Find the MEntity ` with `full_name`. + fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do + if full_name == null then return null + return model.mentity_by_full_name(full_name.from_percent_encoding) + end + # Init the model view from the `req` uri parameters. fun init_model_view(req: HttpRequest): ModelView do var view = new ModelView(model) - var show_private = req.bool_arg("private") or else false if not show_private then view.min_visibility = protected_visibility @@ -111,7 +50,12 @@ end # A NitView is rendered by an action. interface NitView # Renders this view and returns something that can be written to a HTTP response. - fun render(srv: NitServer): Writable is abstract + fun render: Writable is abstract +end + +redef class HttpResponse + # Render a NitView as response. + fun send_view(view: NitView, status: nullable Int) do send(view.render, status) end redef class HttpRequest diff --git a/src/web/web_views.nit b/src/web/web_views.nit index 950c839..58016e2 100644 --- a/src/web/web_views.nit +++ b/src/web/web_views.nit @@ -27,7 +27,7 @@ class HtmlHomePage # Loaded model to display. var tree: MEntityTree - redef fun render(srv) do + redef fun render do var tpl = new Template tpl.add new Header(1, "Loaded model") tpl.add tree.html_list @@ -45,7 +45,7 @@ class HtmlResultPage # Result set var results: Array[MEntity] - redef fun render(srv) do + redef fun render do var tpl = new Template tpl.add new Header(1, "Results for {query}") if results.is_empty then @@ -55,7 +55,7 @@ class HtmlResultPage var list = new UnorderedList for mentity in results do var link = mentity.html_link - link.text = mentity.html_raw_namespace + link.text = mentity.html_full_name list.add_li new ListItem(link) end tpl.add list @@ -76,7 +76,7 @@ class HtmlSourcePage # HiglightVisitor used to hilight the source code var hl = new HighlightVisitor - redef fun render(srv) do + redef fun render do var tpl = new Template tpl.add new Header(1, "Source Code") tpl.add render_source @@ -103,7 +103,7 @@ end class HtmlDocPage super HtmlSourcePage - redef fun render(srv) do + redef fun render do var tpl = new Template tpl.add new Header(1, mentity.html_name) tpl.add "

" @@ -130,7 +130,7 @@ class HtmlDotPage # Page title. var title: String - redef fun render(srv) do + redef fun render do var tpl = new Template tpl.add new Header(1, title) tpl.add render_dot diff --git a/tests/base_redef.nit b/tests/base_redef.nit new file mode 100644 index 0000000..d42bfec --- /dev/null +++ b/tests/base_redef.nit @@ -0,0 +1,65 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import core::kernel + +class A + fun f do 1.output + fun f2: Int do return 2 + fun f2=(i:Int) do i.output + fun f3=(i:Int) do i.output + var v = 4 + type T: A + fun t(t: T): T do return t +end + +class B + super A#alt1# + redef fun f do 10.output + redef var f2 = 20 + var f3 = 30 is redef writable + redef var v = 40 + redef type T: B +end + +class C + super B#alt2# + redef fun f do 100.output + redef var f2 = 200 + redef var f3 = 300 + redef var v = 400 + redef type T: C + +end + +var a = new A +a.f +a.f2 = -2 +a.f2.output +a.f3 = -3 +a.t(a).f + +a = new B +a.f +a.f2 = -2 +a.f2.output +a.f3 = -3 +a.t(a).f + +a = new C +a.f +a.f2 = -2 +a.f2.output +a.f3 = -3 +a.t(a).f diff --git a/tests/listfull.sh b/tests/listfull.sh index 30c792b..70e648a 100755 --- a/tests/listfull.sh +++ b/tests/listfull.sh @@ -1,5 +1,5 @@ #!/bin/sh -printf "%s\n" "$@" \ +ls -1 -- "%s\n" "$@" \ ../src/nit*.nit \ ../src/test_*.nit \ ../src/examples/*.nit \ @@ -9,11 +9,11 @@ printf "%s\n" "$@" \ ../examples/*/src/*_android.nit \ ../examples/*/src/*_linux.nit \ ../examples/*/src/*_null.nit \ - ../examples/nitcorn/src/*.nit \ ../lib/*/examples/*.nit \ ../lib/*/examples/*/*.nit \ ../contrib/friendz/src/solver_cmd.nit \ ../contrib/neo_doxygen/src/tests/neo_doxygen_*.nit \ ../contrib/pep8analysis/src/pep8analysis.nit \ ../contrib/nitiwiki/src/nitiwiki.nit \ - *.nit + *.nit \ + | grep -v ../lib/popcorn/examples/ diff --git a/tests/sav/base_redef.res b/tests/sav/base_redef.res new file mode 100644 index 0000000..3b792d6 --- /dev/null +++ b/tests/sav/base_redef.res @@ -0,0 +1,11 @@ +1 +-2 +2 +-3 +1 +10 +-2 +10 +100 +-2 +100 diff --git a/tests/sav/base_redef_alt1.res b/tests/sav/base_redef_alt1.res new file mode 100644 index 0000000..50e360c --- /dev/null +++ b/tests/sav/base_redef_alt1.res @@ -0,0 +1,5 @@ +alt/base_redef_alt1.nit:29,12: Error: no property `B::f` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt1.nit:30,12--13: Error: no property `B::f2` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt1.nit:31,6--7: Error: no property `B::f3=` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt1.nit:32,12: Error: no property `B::v` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt1.nit:33,2--16: Error: no property `B::T` is inherited. Remove the `redef` keyword to define a new property. diff --git a/tests/sav/base_redef_alt2.res b/tests/sav/base_redef_alt2.res new file mode 100644 index 0000000..1c263c4 --- /dev/null +++ b/tests/sav/base_redef_alt2.res @@ -0,0 +1,5 @@ +alt/base_redef_alt2.nit:38,12: Error: no property `C::f` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt2.nit:39,12--13: Error: no property `C::f2` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt2.nit:40,12--13: Error: no property `C::f3` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt2.nit:41,12: Error: no property `C::v` is inherited. Remove the `redef` keyword to define a new property. +alt/base_redef_alt2.nit:42,2--16: Error: no property `C::T` is inherited. Remove the `redef` keyword to define a new property. diff --git a/tests/sav/nitcatalog_args1.res b/tests/sav/nitcatalog_args1.res index 143e589..19b015f 100644 --- a/tests/sav/nitcatalog_args1.res +++ b/tests/sav/nitcatalog_args1.res @@ -73,6 +73,11 @@

  • https://github.com/nitlang/nit/tree/master/tests/test_prog
  • https://github.com/nitlang/nit.git
  • +

    Quality

    +
      +
    • 28 warnings (63/kloc)
    • +
    • 93% documented
    • +

    Tags

    test, game

    Requirements

    none

    Clients

    diff --git a/tests/sav/nitlight_args1.res b/tests/sav/nitlight_args1.res index 20d487e..fdae4a5 100644 --- a/tests/sav/nitlight_args1.res +++ b/tests/sav/nitlight_args1.res @@ -33,7 +33,7 @@ class B var val: Int - init(v: Int) + init(v: Int) do 7.output self.val = v diff --git a/tests/sav/nitunit_args1.res b/tests/sav/nitunit_args1.res index 58aa2a8..48642c1 100644 --- a/tests/sav/nitunit_args1.res +++ b/tests/sav/nitunit_args1.res @@ -1,6 +1,8 @@ -test_nitunit.nit:20,1--22,0: ERROR: nitunit.test_nitunit::test_nitunit.test_nitunit::X. (in .nitunit/test_nitunit-2.nit): Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5) +test_nitunit.nit:21,7--22,0: ERROR: test_nitunit$X (in .nitunit/test_nitunit-2.nit): +Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5) -test_nitunit.nit:23,2--25,0: FAILURE: nitunit.test_nitunit::test_nitunit.test_nitunit::X.test_nitunit::X::foo (in .nitunit/test_nitunit-3.nit): .nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`. +test_nitunit.nit:24,8--25,0: FAILURE: test_nitunit$X$foo (in .nitunit/test_nitunit-3.nit): +.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`. test_test_nitunit.nit:36,2--40,4: ERROR: test_foo1 (in file .nitunit/gen_test_test_nitunit.nit): Runtime error: Assert failed (test_test_nitunit.nit:39) @@ -9,10 +11,9 @@ Entities: 27; Documented ones: 3; With nitunits: 3; Failures: 2 TestSuites: Class suites: 1; Test Cases: 3; Failures: 1 -assert true -assert false -assert undefined_identifier -outoutout \ No newline at end of file +assert true +assert false +Compilation Error.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`. +assert undefined_identifier +Runtime ErrorRuntime error: Assert failed (test_test_nitunit.nit:39) + \ No newline at end of file diff --git a/tests/sav/nitunit_args4.res b/tests/sav/nitunit_args4.res index 4f4ab53..4cb3aaa 100644 --- a/tests/sav/nitunit_args4.res +++ b/tests/sav/nitunit_args4.res @@ -5,17 +5,17 @@ Entities: 4; Documented ones: 3; With nitunits: 3; Failures: 0 TestSuites: No test cases found Class suites: 0; Test Cases: 0; Failures: 0 -if true then +if true then assert true end -if true then +if true then assert true end -var a = 1 +var a = 1 assert a == 1 assert a == 1 \ No newline at end of file diff --git a/tests/sav/nitunit_args5.res b/tests/sav/nitunit_args5.res index dd4f11a..8fa1c3c 100644 --- a/tests/sav/nitunit_args5.res +++ b/tests/sav/nitunit_args5.res @@ -5,7 +5,7 @@ Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 0 TestSuites: No test cases found Class suites: 0; Test Cases: 0; Failures: 0 -assert true # tested -assert true # tested -assert true # tested +assert true # tested +assert true # tested +assert true # tested \ No newline at end of file diff --git a/tests/sav/nitunit_args6.res b/tests/sav/nitunit_args6.res index b0a65a1..ca7ee6a 100644 --- a/tests/sav/nitunit_args6.res +++ b/tests/sav/nitunit_args6.res @@ -1,5 +1,6 @@ -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>. (in .nitunit/test_nitunit3-0.nit): Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7) +test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\]. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). +test_nitunit3/README.md:4,2--15,0: ERROR: test_nitunit3> (in .nitunit/test_nitunit3-0.nit): Runtime error +Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7) DocUnits: Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2 @@ -7,8 +8,9 @@ Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2 TestSuites: No test cases found Class suites: 0; Test Cases: 0; Failures: 0 -assert false +Compilation ErrorRuntime error: Assert failed (.nitunit/test_nitunit3-0.nit:7) +assert false assert true -assert true +Compilation Errortest_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\].;'\][] +assert true \ No newline at end of file diff --git a/tests/sav/nitunit_args7.res b/tests/sav/nitunit_args7.res index fc20a76..0888b04 100644 --- a/tests/sav/nitunit_args7.res +++ b/tests/sav/nitunit_args7.res @@ -1,4 +1,5 @@ -test_nitunit_md.md:1,0--15,0: ERROR: nitunit..test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error: Assert failed (.nitunit/file-0.nit:8) +test_nitunit_md.md:4,2--16,0: ERROR: nitunit..test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error +Runtime error: Assert failed (.nitunit/file-0.nit:8) DocUnits: Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1 @@ -6,8 +7,8 @@ Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1 TestSuites: No test cases found Class suites: 0; Test Cases: 0; Failures: 0 -var a = 1 +Compilation ErrorRuntime error: Assert failed (.nitunit/file-0.nit:8) +var a = 1 assert 1 == 1 assert false - \ No newline at end of file + \ No newline at end of file diff --git a/tests/sav/nitunit_args8.res b/tests/sav/nitunit_args8.res index 7e2dc83..afa1ac3 100644 --- a/tests/sav/nitunit_args8.res +++ b/tests/sav/nitunit_args8.res @@ -1,10 +1,13 @@ -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'.. +test_doc3.nit:17,9--15: Syntax Error: unexpected identifier 'garbage'. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). +test_doc3.nit:23,4--10: Syntax Error: unexpected identifier 'garbage'. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). +test_doc3.nit:30,4--10: Syntax Error: unexpected identifier 'garbage'. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). DocUnits: Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 3 TestSuites: No test cases found Class suites: 0; Test Cases: 0; Failures: 0 - \ No newline at end of file +Compilation Errortest_doc3.nit:17,9--15: Syntax Error: unexpected identifier 'garbage'. *garbage* +Compilation Errortest_doc3.nit:23,4--10: Syntax Error: unexpected identifier 'garbage'.*garbage* +Compilation Errortest_doc3.nit:30,4--10: Syntax Error: unexpected identifier 'garbage'.*garbage* + \ No newline at end of file diff --git a/tests/sav/nitunit_args9.res b/tests/sav/nitunit_args9.res index 42cbaa9..27d74a9 100644 --- a/tests/sav/nitunit_args9.res +++ b/tests/sav/nitunit_args9.res @@ -1,16 +1,33 @@ -test_nitunit4/test_nitunit4.nit:22,2--25,4: ERROR: test_foo (in file .nitunit/gen_test_nitunit4.nit): Before Test +test_nitunit4/test_nitunit4.nit:22,2--26,4: ERROR: test_foo (in file .nitunit/gen_test_nitunit4.nit): Before Test Tested method After Test Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31) +test_nitunit4/test_nitunit4.nit:32,2--34,4: ERROR: test_baz (in file .nitunit/gen_test_nitunit4.nit): Diff +--- expected:test_nitunit4/test_nitunit4.sav/test_baz.res ++++ got:.nitunit/gen_test_nitunit4_test_baz.out1 +@@ -1 +1,3 @@ +-Bad result file ++Before Test ++Tested method ++After Test + DocUnits: No doc units found -Entities: 10; Documented ones: 0; With nitunits: 0; Failures: 0 +Entities: 12; Documented ones: 0; With nitunits: 0; Failures: 0 TestSuites: -Class suites: 1; Test Cases: 1; Failures: 1 -outRuntime ErrorBefore Test Tested method After Test Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31) -"> \ No newline at end of file +Runtime ErrorDiff +--- 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 + \ No newline at end of file diff --git a/tests/sav/test_copy_to_native.res b/tests/sav/test_copy_to_native.res new file mode 100644 index 0000000..764d170 --- /dev/null +++ b/tests/sav/test_copy_to_native.res @@ -0,0 +1 @@ +gâštr diff --git a/tests/sav/test_copy_to_native_alt1.res b/tests/sav/test_copy_to_native_alt1.res new file mode 100644 index 0000000..785cb5d --- /dev/null +++ b/tests/sav/test_copy_to_native_alt1.res @@ -0,0 +1 @@ +gâštr%D diff --git a/tests/sav/test_copy_to_native_alt2.res b/tests/sav/test_copy_to_native_alt2.res new file mode 100644 index 0000000..ec88d9a --- /dev/null +++ b/tests/sav/test_copy_to_native_alt2.res @@ -0,0 +1 @@ +gâštré diff --git a/tests/sav/test_highlight_args1.res b/tests/sav/test_highlight_args1.res index fe9ca03..449fa7f 100644 --- a/tests/sav/test_highlight_args1.res +++ b/tests/sav/test_highlight_args1.res @@ -53,7 +53,7 @@

    base_simple3$B$val=

    	var val: Int

    base_simple3$B$init

    -
    	init(v: Int)
    +
    	init(v: Int)
     	do
     		7.output
     		self.val = v
    @@ -113,7 +113,7 @@
     
     class B
     	var val: Int
    -	init(v: Int)
    +	init(v: Int)
     	do
     		7.output
     		self.val = v
    diff --git a/tests/sav/test_json_deserialization_plain.res b/tests/sav/test_json_deserialization_plain.res
    index eb9da24..71dc9e7 100644
    --- a/tests/sav/test_json_deserialization_plain.res
    +++ b/tests/sav/test_json_deserialization_plain.res
    @@ -27,6 +27,6 @@
     # Nit: >
     
     # 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
     
    diff --git a/tests/sav/test_model_visitor_args1.res b/tests/sav/test_model_visitor_args1.res
    index c19ddce..fb0ab9b 100644
    --- a/tests/sav/test_model_visitor_args1.res
    +++ b/tests/sav/test_model_visitor_args1.res
    @@ -110,58 +110,58 @@ Names:
       base_simple3: 12 (1.06%)
     
     # All entities
    -base_simple3	MPackage	
    -base_simple3>	MGroup	
    -base_simple3::base_simple3	MModule	
    -base_simple3::Object	MClass	
    -base_simple3$Object	MClassDef	
    -base_simple3::Object::init	MMethod	
    -base_simple3$Object$init	MMethodDef	
    -base_simple3::Bool	MClass	
    -base_simple3$Bool	MClassDef	
    -base_simple3::Int	MClass	
    -base_simple3$Int	MClassDef	
    -base_simple3::Int::output	MMethod	
    -base_simple3$Int$output	MMethodDef	
    -base_simple3::A	MClass	
    -base_simple3$A	MClassDef	
    -base_simple3$A$Object::init	MMethodDef	
    -base_simple3::A::run	MMethod	
    -base_simple3$A$run	MMethodDef	
    -base_simple3::B	MClass	
    -base_simple3$B	MClassDef	
    -base_simple3::base_simple3::B::_val	MAttribute	
    -base_simple3$B$_val	MAttributeDef	
    -base_simple3::B::val	MMethod	
    -base_simple3$B$val	MMethodDef	
    -base_simple3::B::val=	MMethod	
    -base_simple3$B$val=	MMethodDef	
    -base_simple3::B::init	MMethod	
    -base_simple3$B$init	MMethodDef	
    -base_simple3::B::run	MMethod	
    -base_simple3$B$run	MMethodDef	
    -base_simple3::C	MClass	
    -base_simple3$C	MClassDef	
    -base_simple3::base_simple3::C::_val1	MAttribute	
    -base_simple3$C$_val1	MAttributeDef	
    -base_simple3::C::val1	MMethod	
    -base_simple3$C$val1	MMethodDef	
    -base_simple3::C::val1=	MMethod	
    -base_simple3$C$val1=	MMethodDef	
    -base_simple3::base_simple3::C::_val2	MAttribute	
    -base_simple3$C$_val2	MAttributeDef	
    -base_simple3::C::val2	MMethod	
    -base_simple3$C$val2	MMethodDef	
    -base_simple3::C::val2=	MMethod	
    -base_simple3$C$val2=	MMethodDef	
    -base_simple3$C$Object::init	MMethodDef	
    -base_simple3::Sys	MClass	
    -base_simple3$Sys	MClassDef	
    -base_simple3::Sys::foo	MMethod	
    -base_simple3$Sys$foo	MMethodDef	
    -base_simple3::Sys::bar	MMethod	
    -base_simple3$Sys$bar	MMethodDef	
    -base_simple3::Sys::baz	MMethod	
    -base_simple3$Sys$baz	MMethodDef	
    -base_simple3::Sys::main	MMethod	
    -base_simple3$Sys$main	MMethodDef	
    +base_simple3	MPackage	base_simple3.nit	
    +base_simple3>	MGroup	base_simple3.nit	
    +base_simple3::base_simple3	MModule	base_simple3.nit:17,1--66,13	
    +base_simple3::Object	MClass	base_simple3.nit:19,1--20,3	
    +base_simple3$Object	MClassDef	base_simple3.nit:19,1--20,3	
    +base_simple3::Object::init	MMethod	base_simple3.nit:19,1--20,3	
    +base_simple3$Object$init	MMethodDef	base_simple3.nit:19,1--20,3	
    +base_simple3::Bool	MClass	base_simple3.nit:22,1--23,3	
    +base_simple3$Bool	MClassDef	base_simple3.nit:22,1--23,3	
    +base_simple3::Int	MClass	base_simple3.nit:25,1--27,3	
    +base_simple3$Int	MClassDef	base_simple3.nit:25,1--27,3	
    +base_simple3::Int::output	MMethod	base_simple3.nit:26,2--21	
    +base_simple3$Int$output	MMethodDef	base_simple3.nit:26,2--21	
    +base_simple3::A	MClass	base_simple3.nit:29,1--32,3	
    +base_simple3$A	MClassDef	base_simple3.nit:29,1--32,3	
    +base_simple3$A$Object::init	MMethodDef	base_simple3.nit:30,2--17	
    +base_simple3::A::run	MMethod	base_simple3.nit:31,2--20	
    +base_simple3$A$run	MMethodDef	base_simple3.nit:31,2--20	
    +base_simple3::B	MClass	base_simple3.nit:34,1--42,3	
    +base_simple3$B	MClassDef	base_simple3.nit:34,1--42,3	
    +base_simple3::base_simple3::B::_val	MAttribute	base_simple3.nit:35,2--13	
    +base_simple3$B$_val	MAttributeDef	base_simple3.nit:35,2--13	
    +base_simple3::B::val	MMethod	base_simple3.nit:35,2--13	
    +base_simple3$B$val	MMethodDef	base_simple3.nit:35,2--13	
    +base_simple3::B::val=	MMethod	base_simple3.nit:35,2--13	
    +base_simple3$B$val=	MMethodDef	base_simple3.nit:35,2--13	
    +base_simple3::B::init	MMethod	base_simple3.nit:36,2--40,4	
    +base_simple3$B$init	MMethodDef	base_simple3.nit:36,2--40,4	
    +base_simple3::B::run	MMethod	base_simple3.nit:41,2--22	
    +base_simple3$B$run	MMethodDef	base_simple3.nit:41,2--22	
    +base_simple3::C	MClass	base_simple3.nit:44,1--47,3	
    +base_simple3$C	MClassDef	base_simple3.nit:44,1--47,3	
    +base_simple3::base_simple3::C::_val1	MAttribute	base_simple3.nit:45,2--14	
    +base_simple3$C$_val1	MAttributeDef	base_simple3.nit:45,2--14	
    +base_simple3::C::val1	MMethod	base_simple3.nit:45,2--14	
    +base_simple3$C$val1	MMethodDef	base_simple3.nit:45,2--14	
    +base_simple3::C::val1=	MMethod	base_simple3.nit:45,2--14	
    +base_simple3$C$val1=	MMethodDef	base_simple3.nit:45,2--14	
    +base_simple3::base_simple3::C::_val2	MAttribute	base_simple3.nit:46,2--19	
    +base_simple3$C$_val2	MAttributeDef	base_simple3.nit:46,2--19	
    +base_simple3::C::val2	MMethod	base_simple3.nit:46,2--19	
    +base_simple3$C$val2	MMethodDef	base_simple3.nit:46,2--19	
    +base_simple3::C::val2=	MMethod	base_simple3.nit:46,2--19	
    +base_simple3$C$val2=	MMethodDef	base_simple3.nit:46,2--19	
    +base_simple3$C$Object::init	MMethodDef	base_simple3.nit:44,1--47,3	
    +base_simple3::Sys	MClass	base_simple3.nit:49,1--19	
    +base_simple3$Sys	MClassDef	base_simple3.nit:49,1--19	
    +base_simple3::Sys::foo	MMethod	base_simple3.nit:49,1--19	
    +base_simple3$Sys$foo	MMethodDef	base_simple3.nit:49,1--19	
    +base_simple3::Sys::bar	MMethod	base_simple3.nit:50,1--27	
    +base_simple3$Sys$bar	MMethodDef	base_simple3.nit:50,1--27	
    +base_simple3::Sys::baz	MMethod	base_simple3.nit:51,1--24	
    +base_simple3$Sys$baz	MMethodDef	base_simple3.nit:51,1--24	
    +base_simple3::Sys::main	MMethod	base_simple3.nit:53,1--66,13	
    +base_simple3$Sys$main	MMethodDef	base_simple3.nit:53,1--66,13	
    diff --git a/tests/sav/test_model_visitor_args2.res b/tests/sav/test_model_visitor_args2.res
    index 2dd778a..caad05a 100644
    --- a/tests/sav/test_model_visitor_args2.res
    +++ b/tests/sav/test_model_visitor_args2.res
    @@ -106,101 +106,101 @@ Names:
       names: 5 (0.28%)
     
     # All entities
    -names	MPackage	Group of modules used to test various full_name configurations and conflicts.
    -names>	MGroup	Group of modules used to test various full_name configurations and conflicts.
    -names::n3	MModule	The bottom module
    -names::n3$A1	MClassDef	a refinement of a subclass in a submodule
    -names::n3$A1$A::a	MMethodDef	a refinement (3 distinct modules)
    -names::n3$A1$::n0::P::p	MMethodDef	a refinement (3 distinct modules)
    -names::n3$::n1::P1	MClassDef	a refinement of a subclass in a submodule
    -names::n3$::n1::P1$A::a	MMethodDef	a refinement (3 distinct modules)
    -names::n3$::n1::P1$::n0::P::p	MMethodDef	a refinement (3 distinct modules)
    -names::n0	MModule	Root module
    -names::Object	MClass	
    -names$Object	MClassDef	Root interface
    -names::Object::init	MMethod	
    -names$Object$init	MMethodDef	
    -names::A	MClass	
    -names$A	MClassDef	A public class
    -names::A::a	MMethod	
    -names$A$a	MMethodDef	A public method in a public class
    -names::n0::A::z	MMethod	
    -names$A$z	MMethodDef	A private method in a public class
    -names::A0	MClass	
    -names$A0	MClassDef	A public subclass in the same module
    -names$A0$A::a	MMethodDef	Redefinition it the same module of a public method
    -names$A0$::n0::A::z	MMethodDef	Redefinition it the same module of a private method
    -names$A0$::n0::P::p	MMethodDef	Redefinition it the same module of a private method
    -names::n0::P	MClass	
    -names::n0$P	MClassDef	A private class
    -names::n0::P::p	MMethod	
    -names::n0$P$p	MMethodDef	A private method in a private class
    -names::n0::P0	MClass	
    -names::n0$P0	MClassDef	A private subclass introduced in the same module
    -names::n0$P0$A::a	MMethodDef	Redefinition it the same module of a public method
    -names::n0$P0$::n0::A::z	MMethodDef	Redefinition it the same module of a private method
    -names::n0$P0$::n0::P::p	MMethodDef	Redefinition it the same module of a private method
    -names::n1	MModule	Second module
    -names::n1$A	MClassDef	A refinement of a class
    -names::n1$A$a	MMethodDef	A refinement in the same class
    -names::n1$A$z	MMethodDef	A refinement in the same class
    -names::n1::A::b	MMethod	
    -names::n1$A$b	MMethodDef	A public method introduced in a refinement
    -names::n1$A0	MClassDef	A refinement of a subclass
    -names::n1$A0$A::a	MMethodDef	A refinement+redefinition
    -names::n1$A0$::n0::A::z	MMethodDef	A refinement+redefinition
    -names::n1$A0$::n0::P::p	MMethodDef	A refinement+redefinition
    -names::A1	MClass	
    -names$A1	MClassDef	A subclass introduced in a submodule
    -names$A1$A::a	MMethodDef	A redefinition in a subclass from a different module
    -names$A1$::n0::A::z	MMethodDef	A redefinition in a subclass from a different module
    -names$A1$::n0::P::p	MMethodDef	A redefinition in a subclass from a different module
    -names::n1$::n0::P	MClassDef	A refinement of a class
    -names::n1$::n0::P$p	MMethodDef	A refinement in the same class
    -names::n1$::n0::P0	MClassDef	A refinement of a subclass
    -names::n1$::n0::P0$A::a	MMethodDef	A refinement+redefinition
    -names::n1$::n0::P0$::n0::A::z	MMethodDef	A refinement+redefinition
    -names::n1$::n0::P0$::n0::P::p	MMethodDef	A refinement+redefinition
    -names::n1::P1	MClass	
    -names::n1$P1	MClassDef	A private subclass introduced in a different module
    -names::n1$P1$A::a	MMethodDef	A redefinition in a subclass from a different module
    -names::n1$P1$::n0::A::z	MMethodDef	A redefinition in a subclass from a different module
    -names::n1$P1$::n0::P::p	MMethodDef	A redefinition in a subclass from a different module
    -names::n2	MModule	A alternative second module, used to make name conflicts
    -names::n2$A	MClassDef	A refinement of a class
    -names::n2::A::b	MMethod	
    -names::n2$A$b	MMethodDef	Name conflict? A second public method
    -names::n2::A::z	MMethod	
    -names::n2$A$z	MMethodDef	Name conflict? A second private method
    -names::n2::P	MClass	
    -names::n2$P	MClassDef	Name conflict? A second private class
    -names::n2::P::p	MMethod	
    -names::n2$P$p	MMethodDef	Name conflict? A private method in an homonym class.
    -names1	MPackage	An alternative second module in a distinct package
    -names1>	MGroup	An alternative second module in a distinct package
    -names1::names1	MModule	An alternative second module in a distinct package
    -names1::names1$names::A	MClassDef	A refinement of a class
    -names1::names1$names::A$a	MMethodDef	A refinement in the same class
    -names1::names1$names::A$z	MMethodDef	A refinement in the same class
    -names1::names1::A::b	MMethod	
    -names1::names1$names::A$b	MMethodDef	A public method introduced in a refinement
    -names1::names1$names::A0	MClassDef	A refinement of a subclass
    -names1::names1$names::A0$names::A::a	MMethodDef	A refinement+redefinition
    -names1::names1$names::A0$names::n0::A::z	MMethodDef	A refinement+redefinition
    -names1::names1$names::A0$names::n0::P::p	MMethodDef	A refinement+redefinition
    -names1::A1	MClass	
    -names1$A1	MClassDef	A subclass introduced in a submodule
    -names1$A1$names::A::a	MMethodDef	A redefinition in a subclass from a different module
    -names1$A1$names::n0::A::z	MMethodDef	A redefinition in a subclass from a different module
    -names1$A1$names::n0::P::p	MMethodDef	A redefinition in a subclass from a different module
    -names1::names1$names::n0::P	MClassDef	A refinement of a class
    -names1::names1$names::n0::P$p	MMethodDef	A refinement in the same class
    -names1::names1$names::n0::P0	MClassDef	A refinement of a subclass
    -names1::names1$names::n0::P0$names::A::a	MMethodDef	A refinement+redefinition
    -names1::names1$names::n0::P0$names::n0::A::z	MMethodDef	A refinement+redefinition
    -names1::names1$names::n0::P0$names::n0::P::p	MMethodDef	A refinement+redefinition
    -names1::names1::P1	MClass	
    -names1::names1$P1	MClassDef	A private subclass introduced in a different module
    -names1::names1$P1$names::A::a	MMethodDef	A redefinition in a subclass from a different module
    -names1::names1$P1$names::n0::A::z	MMethodDef	A redefinition in a subclass from a different module
    -names1::names1$P1$names::n0::P::p	MMethodDef	A redefinition in a subclass from a different module
    +names	MPackage	names	Group of modules used to test various full_name configurations and conflicts.
    +names>	MGroup	names	Group of modules used to test various full_name configurations and conflicts.
    +names::n3	MModule	names/n3.nit:15,1--35,3	The bottom module
    +names::n3$A1	MClassDef	names/n3.nit:21,1--27,3	a refinement of a subclass in a submodule
    +names::n3$A1$A::a	MMethodDef	names/n3.nit:23,2--24,19	a refinement (3 distinct modules)
    +names::n3$A1$::n0::P::p	MMethodDef	names/n3.nit:25,2--26,19	a refinement (3 distinct modules)
    +names::n3$::n1::P1	MClassDef	names/n3.nit:29,1--35,3	a refinement of a subclass in a submodule
    +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	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	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	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$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	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	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	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	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::n0$P0$::n0::P::p	MMethodDef	names/n0.nit:65,2--66,19	Redefinition it the same module of a private method
    +names::n1	MModule	names/n1.nit:15,1--90,3	Second module
    +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	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	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$A1$::n0::P::p	MMethodDef	names/n1.nit:55,2--56,19	A redefinition in a subclass from a different module
    +names::n1$::n0::P	MClassDef	names/n1.nit:59,1--63,3	A refinement of a class
    +names::n1$::n0::P$p	MMethodDef	names/n1.nit:61,2--62,19	A refinement in the same class
    +names::n1$::n0::P0	MClassDef	names/n1.nit:65,1--75,3	A refinement of a subclass
    +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	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	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	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	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	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	MModule	names1.nit:15,1--90,3	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$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	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$A1$names::n0::P::p	MMethodDef	names1.nit:55,2--56,19	A redefinition in a subclass from a different module
    +names1::names1$names::n0::P	MClassDef	names1.nit:59,1--63,3	A refinement of a class
    +names1::names1$names::n0::P$p	MMethodDef	names1.nit:61,2--62,19	A refinement in the same class
    +names1::names1$names::n0::P0	MClassDef	names1.nit:65,1--75,3	A refinement of a subclass
    +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	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
    +names1::names1$P1$names::n0::P::p	MMethodDef	names1.nit:88,2--89,19	A redefinition in a subclass from a different module
    diff --git a/tests/sav/test_nativestring_fill_from.res b/tests/sav/test_nativestring_fill_from.res
    new file mode 100644
    index 0000000..2c26c70
    --- /dev/null
    +++ b/tests/sav/test_nativestring_fill_from.res
    @@ -0,0 +1 @@
    +S&éstr
    diff --git a/tests/sav/test_nativestring_fill_from_alt1.res b/tests/sav/test_nativestring_fill_from_alt1.res
    new file mode 100644
    index 0000000..0667158
    --- /dev/null
    +++ b/tests/sav/test_nativestring_fill_from_alt1.res
    @@ -0,0 +1 @@
    +S&éstrS&éstr
    diff --git a/tests/sav/test_nativestring_fill_from_alt2.res b/tests/sav/test_nativestring_fill_from_alt2.res
    new file mode 100644
    index 0000000..2c26c70
    --- /dev/null
    +++ b/tests/sav/test_nativestring_fill_from_alt2.res
    @@ -0,0 +1 @@
    +S&éstr
    diff --git a/tests/sav/test_nativestring_fill_from_alt3.res b/tests/sav/test_nativestring_fill_from_alt3.res
    new file mode 100644
    index 0000000..f28809f
    --- /dev/null
    +++ b/tests/sav/test_nativestring_fill_from_alt3.res
    @@ -0,0 +1 @@
    +&éstr
    diff --git a/tests/sav/test_postgres_native.res b/tests/sav/test_postgres_native.res
    new file mode 100644
    index 0000000..0c511fe
    --- /dev/null
    +++ b/tests/sav/test_postgres_native.res
    @@ -0,0 +1,3 @@
    +aname   class   sex   
    +Whale   mammal   1   
    +Snake   reptile   0   
    diff --git a/tests/sav/xymus_net.res b/tests/sav/test_postgres_nity.res
    similarity index 100%
    rename from tests/sav/xymus_net.res
    rename to tests/sav/test_postgres_nity.res
    diff --git a/tests/sav/test_realtime.res b/tests/sav/test_realtime.res
    deleted file mode 100644
    index 65b7078..0000000
    --- a/tests/sav/test_realtime.res
    +++ /dev/null
    @@ -1,7 +0,0 @@
    -sleeping 1s
    -true
    -true
    -sleeping 5000ns
    -true
    -true
    -true
    diff --git a/tests/sav/test_substring.res b/tests/sav/test_substring.res
    index f40c047..54cb7f8 100644
    --- a/tests/sav/test_substring.res
    +++ b/tests/sav/test_substring.res
    @@ -43,3 +43,6 @@ test.has_suffix("test") => true
     test.has_suffix("bt") => false
     test.has_suffix("bat") => false
     test.has_suffix("foot") => false
    +........
    +
    +........
    diff --git a/tests/test_copy_to_native.nit b/tests/test_copy_to_native.nit
    new file mode 100644
    index 0000000..380c02a
    --- /dev/null
    +++ b/tests/test_copy_to_native.nit
    @@ -0,0 +1,28 @@
    +# This file is part of NIT ( http://www.nitlanguage.org ).
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License");
    +# you may not use this file except in compliance with the License.
    +# You may obtain a copy of the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS,
    +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +# See the License for the specific language governing permissions and
    +# limitations under the License.
    +
    +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)
    diff --git a/tests/test_kill_process.nit b/tests/test_kill_process.nit
    index 21728c1..a9f3e61 100644
    --- a/tests/test_kill_process.nit
    +++ b/tests/test_kill_process.nit
    @@ -21,12 +21,11 @@ var clock = new Clock
     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
    diff --git a/tests/test_nativestring_fill_from.nit b/tests/test_nativestring_fill_from.nit
    new file mode 100644
    index 0000000..1b9e491
    --- /dev/null
    +++ b/tests/test_nativestring_fill_from.nit
    @@ -0,0 +1,27 @@
    +# 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)
    diff --git a/tests/test_nitunit4/test_nitunit4.nit b/tests/test_nitunit4/test_nitunit4.nit
    index db734a6..cd13aca 100644
    --- a/tests/test_nitunit4/test_nitunit4.nit
    +++ b/tests/test_nitunit4/test_nitunit4.nit
    @@ -22,5 +22,14 @@ class TestTestSuite
     	fun test_foo do
     		print "Tested method"
     		assert before
    +		before = false
    +	end
    +
    +	fun test_bar do
    +		print "Tested method"
    +	end
    +
    +	fun test_baz do
    +		print "Tested method"
     	end
     end
    diff --git a/tests/test_nitunit4/test_nitunit4.sav/test_bar.res b/tests/test_nitunit4/test_nitunit4.sav/test_bar.res
    new file mode 100644
    index 0000000..f6d97cf
    --- /dev/null
    +++ b/tests/test_nitunit4/test_nitunit4.sav/test_bar.res
    @@ -0,0 +1,3 @@
    +Before Test
    +Tested method
    +After Test
    diff --git a/tests/test_nitunit4/test_nitunit4.sav/test_baz.res b/tests/test_nitunit4/test_nitunit4.sav/test_baz.res
    new file mode 100644
    index 0000000..0e89bcd
    --- /dev/null
    +++ b/tests/test_nitunit4/test_nitunit4.sav/test_baz.res
    @@ -0,0 +1 @@
    +Bad result file
    diff --git a/tests/test_nitunit4/test_nitunit4_base.nit b/tests/test_nitunit4/test_nitunit4_base.nit
    index 3e7a1e7..af21e73 100644
    --- a/tests/test_nitunit4/test_nitunit4_base.nit
    +++ b/tests/test_nitunit4/test_nitunit4_base.nit
    @@ -28,6 +28,6 @@ class SuperTestSuite
     
     	redef fun after_test do
     		print "After Test"
    -		assert false
    +		assert before
     	end
     end
    diff --git a/tests/test_postgres_native.nit b/tests/test_postgres_native.nit
    new file mode 100644
    index 0000000..ff3be6f
    --- /dev/null
    +++ b/tests/test_postgres_native.nit
    @@ -0,0 +1,73 @@
    +# This file is part of NIT ( http://www.nitlanguage.org ).
    +#
    +# Copyright 2016 Guilherme Mansur 
    +#
    +# 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
    diff --git a/tests/test_postgres_nity.nit b/tests/test_postgres_nity.nit
    new file mode 100644
    index 0000000..9565335
    --- /dev/null
    +++ b/tests/test_postgres_nity.nit
    @@ -0,0 +1,49 @@
    +# This file is part of NIT ( http://www.nitlanguage.org ).
    +#
    +# Copyright 2016 Guilherme Mansur 
    +#
    +# 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
    diff --git a/tests/test_sqlite3_native.nit b/tests/test_sqlite3_native.nit
    index 0573a88..1e16375 100644
    --- a/tests/test_sqlite3_native.nit
    +++ b/tests/test_sqlite3_native.nit
    @@ -29,18 +29,18 @@ var select_req = "SELECT * FROM users"
     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
    @@ -56,8 +56,8 @@ db.close
     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"
    diff --git a/tests/test_substring.nit b/tests/test_substring.nit
    index c2393aa..ed7d73d 100644
    --- a/tests/test_substring.nit
    +++ b/tests/test_substring.nit
    @@ -75,3 +75,6 @@ print("test.has_suffix(\"bt\") => {test.has_suffix("bt")}")
     print("test.has_suffix(\"bat\") => {test.has_suffix("bat")}")
     print("test.has_suffix(\"foot\") => {test.has_suffix("foot")}")
     
    +print "........"
    +print "/".substring(7, 1)
    +print "........"