From: Jean Privat Date: Sat, 21 May 2016 05:40:13 +0000 (-0400) Subject: Merge: src/platforms: fallback to version field "0" on error when asking for a git_re... X-Git-Url: http://nitlanguage.org?hp=d4df7ee205b1559172e3d2a587599b182a25553f Merge: src/platforms: fallback to version field "0" on error when asking for a git_revision The _app.nit_ annotation `version` sets the version of mobile app packages (.apk and .app). For example, `version(1, 5, git_revision)` may produce the version string "1.5.6b42a7c". This PR fixes an error when asking for a `git_revision` but the call to `git rev-parse` fails. Note that the normal git error is printed before the nitc warning message, so debugging should be easy. Close #2111 Pull-Request: #2113 Reviewed-by: Jean Privat --- 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/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/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 100% rename from contrib/benitlux/src/benitlux_controller.nit rename to contrib/benitlux/src/server/benitlux_controller.nit 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 100% rename from contrib/benitlux/src/benitlux_db.nit rename to contrib/benitlux/src/server/benitlux_db.nit 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 98% rename from contrib/benitlux/src/benitlux_web.nit rename to contrib/benitlux/src/server/server.nit index 3b2743b..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 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/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/lib/android/ui/ui.nit b/lib/android/ui/ui.nit index 1c40827..cd9f4ea 100644 --- a/lib/android/ui/ui.nit +++ b/lib/android/ui/ui.nit @@ -244,7 +244,7 @@ redef class TextView else // if (align > 0.5d) g = android.view.Gravity.RIGHT; - view.setGravity(g); + view.setGravity(g | android.view.Gravity.CENTER_VERTICAL); `} end diff --git a/lib/ios/ui/ui.nit b/lib/ios/ui/ui.nit index acf3018..48c7e12 100644 --- a/lib/ios/ui/ui.nit +++ b/lib/ios/ui/ui.nit @@ -112,6 +112,19 @@ redef class App set_view_controller(app_delegate.window, window.native) super end + + # Use iOS ` popViewControllerAnimated` + redef fun pop_window + do + window_stack.pop + pop_view_controller app_delegate.window + window.on_resume + end + + private fun pop_view_controller(window: UIWindow) in "ObjC" `{ + UINavigationController *navController = (UINavigationController*)window.rootViewController; + [navController popViewControllerAnimated: YES]; + `} end redef class AppDelegate @@ -267,8 +280,8 @@ redef class CheckBox init do # Tweak the layout so it is centered - layout.native.distribution = new UIStackViewDistribution.fill_proportionally - layout.native.alignment = new UIStackViewAlignment.center + layout.native.distribution = new UIStackViewDistribution.equal_spacing + layout.native.alignment = new UIStackViewAlignment.fill layout.native.layout_margins_relative_arrangement = true var s = new UISwitch @@ -364,7 +377,7 @@ redef class ListLayout native_stack_view.translates_autoresizing_mask_into_constraits = false native_stack_view.axis = new UILayoutConstraintAxis.vertical native_stack_view.alignment = new UIStackViewAlignment.fill - native_stack_view.distribution = new UIStackViewDistribution.fill_equally + native_stack_view.distribution = new UIStackViewDistribution.equal_spacing native_stack_view.spacing = 4.0 native.add_subview native_stack_view 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/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/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..08f7c9a --- /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})" + 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..b97d10c --- /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})" + 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/lib/popcorn/examples/middlewares/example_simple_error_handler.nit b/lib/popcorn/examples/middlewares/example_simple_error_handler.nit new file mode 100644 index 0000000..16a5977 --- /dev/null +++ b/lib/popcorn/examples/middlewares/example_simple_error_handler.nit @@ -0,0 +1,39 @@ +# 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 SimpleErrorHandler + super Handler + + redef fun all(req, res) do + if res.status_code != 200 then + res.send("An error occurred!", res.status_code) + end + end +end + +class HelloHandler + super Handler + + redef fun get(req, res) do res.send "Hello World!" +end + + +var app = new App +app.use("/", new HelloHandler) +app.use("/*", new SimpleErrorHandler) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/middlewares/example_simple_logger.nit b/lib/popcorn/examples/middlewares/example_simple_logger.nit new file mode 100644 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..38e4bd9 --- /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})" + 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/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 index 655005d..d7a3f49 100644 --- a/misc/docker/Dockerfile +++ b/misc/docker/Dockerfile @@ -1,5 +1,4 @@ -# This is a full install of Nit on a debian base. -# Full because most dependencies are installed so that most tests can be run +# This is a basic install of Nit on a debian base. FROM debian:jessie MAINTAINER Jean Privat @@ -30,37 +29,3 @@ RUN git clone https://github.com/nitlang/nit.git /root/nit \ && strip c_src/nitc bin/nit* \ && ccache -C \ && rm -rf .git - -# Dependencies for more libs and tests -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - # Packages needed for lib/ - libcurl4-openssl-dev \ - libegl1-mesa-dev \ - libevent-dev \ - libgles1-mesa-dev \ - libgles2-mesa-dev \ - libgtk-3-dev \ - libncurses5-dev \ - libsdl-image1.2-dev \ - libsdl-ttf2.0-dev \ - libsdl1.2-dev \ - libsdl2-dev \ - libsqlite3-dev \ - libx11-dev \ - libxdg-basedir-dev \ - # Packages needed for platforms and FFI - default-jdk \ - libopenmpi-dev \ - clang \ - # TODO neo4j android emscripten test_glsl_validation - && rm -rf /var/lib/apt/lists/* - -# Run tests -RUN cd /root/nit/tests \ - && ./testfull.sh || true \ - && rm -rf out/ alt/*.nit \ - && ccache -C -# TODO: nitunits - -WORKDIR /root/nit -ENTRYPOINT [ "bash" ] diff --git a/misc/docker/full/Dockerfile b/misc/docker/full/Dockerfile new file mode 100644 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/share/man/nitunit.md b/share/man/nitunit.md index 4f9ad61..0576201 100644 --- a/share/man/nitunit.md +++ b/share/man/nitunit.md @@ -169,6 +169,34 @@ class TestFoo end ~~~~ +## Black Box Testing + +Sometimes, it is easier to validate a `TestCase` by comparing its output with a text file containing the expected result. + +For each TestCase `test_bar` of a TestSuite `test_mod.nit`, if the corresponding file `test_mod.sav/test_bar.res` exists, then the output of the test is compared with the file. + +The `diff(1)` command is used to perform the comparison. +The test is failed if non-zero is returned by `diff`. + +~~~ +module test_mod is test_suite +class TestFoo + fun test_bar do + print "Hello!" + end +end +~~~ + +Where `test_mod.sav/test_bar.res` contains + +~~~raw +Hello! +~~~ + +If no corresponding `.res` file exists, then the output of the TestCase is ignored. + +## Configuring TestSuites + `TestSuites` also provide methods to configure the test run: `before_test` and `after_test`: methods called before/after each test case. @@ -237,13 +265,19 @@ With the `--full` option, all imported modules (even those in standard) are also ### `-o`, `--output` Output name (default is 'nitunit.xml'). -### `nitunit` produces a XML file comatible with JUnit. +`nitunit` produces a XML file compatible with JUnit. ### `--dir` Working directory (default is '.nitunit'). In order to execute the tests, nit files are generated then compiled and executed in the giver working directory. +### `--nitc` +nitc compiler to use. + +By default, nitunit tries to locate the `nitc` program with the environment variable `NITC` or heuristics. +The option is used to indicate a specific nitc binary. + ### `--no-act` Does not compile and run tests. @@ -271,6 +305,13 @@ Also generate test case for private methods. ### `--only-show` Only display the skeleton, do not write any file. + +# ENVIRONMENT VARIABLES + +### `NITC` + +Indicate the specific Nit compiler executable to use. See `--nitc`. + # SEE ALSO The Nit language documentation and the source code of its tools and libraries may be downloaded from diff --git a/src/catalog.nit b/src/catalog.nit index d8a04ad..1743179 100644 --- a/src/catalog.nit +++ b/src/catalog.nit @@ -235,6 +235,9 @@ class Catalog # Number of warnings and advices var warnings = new Counter[MPackage] + # Number of warnings per 1000 lines of code (w/kloc) + var warnings_per_kloc = new Counter[MPackage] + # Documentation score (between 0 and 100) var documentation_score = new Counter[MPackage] @@ -254,8 +257,13 @@ class Catalog do var p = persons.get_or_null(person) if p == null then - p = new Person.parse(person) - persons[person] = p + var new_p = new Person.parse(person) + # Maybe, we already have this person in fact? + p = persons.get_or_null(new_p.to_s) + if p == null then + p = new_p + persons[p.to_s] = p + end end var projs = contrib2proj[p] if not projs.has(mpackage) then @@ -401,6 +409,9 @@ class Catalog self.loc[mpackage] = loc self.errors[mpackage] = errors self.warnings[mpackage] = warnings + if loc > 0 then + self.warnings_per_kloc[mpackage] = warnings * 1000 / loc + end var documentation_score = (100.0 * doc_score / entity_score).to_i self.documentation_score[mpackage] = documentation_score 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/nitcatalog.nit b/src/nitcatalog.nit index 9066e0f..5c478f0 100644 --- a/src/nitcatalog.nit +++ b/src/nitcatalog.nit @@ -320,7 +320,7 @@ redef class Catalog if errors > 0 then res.add "
  • {errors} errors
  • \n" end - res.add "
  • {warnings[mpackage]} warnings
  • \n" + res.add "
  • {warnings[mpackage]} warnings ({warnings_per_kloc[mpackage]}/kloc)
  • \n" res.add "
  • {documentation_score[mpackage]}% documented
  • \n" res.add "\n" @@ -485,6 +485,7 @@ redef class Catalog 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 @@ -507,6 +508,7 @@ redef class Catalog 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 diff --git a/src/nitunit.nit b/src/nitunit.nit index cc9ea85..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) @@ -65,6 +65,10 @@ end "NIT_TESTING".setenv("true") +var test_dir = toolcontext.test_dir +test_dir.mkdir +"# This file prevents the Nit modules of the directory to be part of the package".write_to_file(test_dir / "packages.ini") + var page = new HTMLTag("testsuites") if toolcontext.opt_full.value then mmodules = model.mmodules 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/testing/testing_base.nit b/src/testing/testing_base.nit index 62cfb08..414de9a 100644 --- a/src/testing/testing_base.nit +++ b/src/testing/testing_base.nit @@ -27,6 +27,8 @@ redef class ToolContext var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir") # opt --no-act var opt_noact = new OptionBool("Does not compile and run tests", "--no-act") + # opt --nitc + var opt_nitc = new OptionString("nitc compiler to use", "--nitc") # Working directory for testing. fun test_dir: String do @@ -34,4 +36,96 @@ redef class ToolContext if dir == null then return ".nitunit" return dir end + + # Search the `nitc` compiler to use + # + # If not `nitc` is suitable, then prints an error and quit. + fun find_nitc: String + do + var nitc = opt_nitc.value + if nitc != null then + if not nitc.file_exists then + fatal_error(null, "error: cannot find `{nitc}` given by --nitc.") + abort + end + return nitc + end + + nitc = "NITC".environ + if nitc != "" then + if not nitc.file_exists then + fatal_error(null, "error: cannot find `{nitc}` given by NITC.") + abort + end + return nitc + end + + var nit_dir = nit_dir + nitc = nit_dir/"bin/nitc" + if not nitc.file_exists then + fatal_error(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.") + abort + end + return nitc + end + + # Execute a system command in a more safe context than `Sys::system`. + fun safe_exec(command: String): Int + do + info(command, 2) + var real_command = """ +bash -c " +ulimit -f {{{ulimit_file}}} 2> /dev/null +ulimit -t {{{ulimit_usertime}}} 2> /dev/null +{{{command}}} +" +""" + return system(real_command) + end + + # The maximum size (in KB) of files written by a command executed trough `safe_exec` + # + # Default: 64MB + var ulimit_file = 65536 is writable + + # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec` + # + # Default: 10 CPU minute + var ulimit_usertime = 600 is writable +end + +redef class String + # If needed, truncate `self` at `max_length` characters and append an informative `message`. + # + # ~~~ + # assert "hello".trunc(10) == "hello" + # assert "hello".trunc(2) == "he[truncated. Full size is 5]" + # assert "hello".trunc(2, "...") == "he..." + # ~~~ + fun trunc(max_length: Int, message: nullable String): String + do + if length <= max_length then return self + if message == null then message = "[truncated. Full size is {length}]" + return substring(0, max_length) + message + end + + # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space). + # + # ~~~ + # assert "hello".filter_nonprintable == "hello" + # assert "\r\n\t".filter_nonprintable == "^13\n^9" + # ~~~ + fun filter_nonprintable: String + do + var buf = new Buffer + for c in self do + var cp = c.code_point + if cp < 32 and c != '\n' then + buf.append "^{cp}" + else + buf.add c + end + end + return buf.to_s + end end diff --git a/src/testing/testing_doc.nit b/src/testing/testing_doc.nit index 2514454..05c1f67 100644 --- a/src/testing/testing_doc.nit +++ b/src/testing/testing_doc.nit @@ -157,14 +157,15 @@ class NitUnitExecutor toolcontext.modelbuilder.unit_entities += 1 i += 1 toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1) - var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 '{file}.out1' 2>&1 >'{file}.out1' 2>&1 '{file}.out1' 2>&1 '{file}.out1' 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/listfull.sh b/tests/listfull.sh index 30c792b..a28ab28 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 \ @@ -16,4 +16,5 @@ printf "%s\n" "$@" \ ../contrib/neo_doxygen/src/tests/neo_doxygen_*.nit \ ../contrib/pep8analysis/src/pep8analysis.nit \ ../contrib/nitiwiki/src/nitiwiki.nit \ - *.nit + *.nit \ + | grep -v ../lib/popcorn/examples/ diff --git a/tests/sav/nitcatalog_args1.res b/tests/sav/nitcatalog_args1.res index 95abffa..19b015f 100644 --- a/tests/sav/nitcatalog_args1.res +++ b/tests/sav/nitcatalog_args1.res @@ -75,7 +75,7 @@

    Quality

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

    Tags

    diff --git a/tests/sav/nitunit_args1.res b/tests/sav/nitunit_args1.res index 58aa2a8..15eee20 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: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:23,2--25,0: FAILURE: nitunit.test_nitunit::test_nitunit.test_nitunit::X.test_nitunit::X::foo (in .nitunit/test_nitunit-3.nit): .nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`. +test_nitunit.nit:23,2--25,0: FAILURE: nitunit.test_nitunit::test_nitunit.test_nitunit::X.test_nitunit::X::foo (in .nitunit/test_nitunit-3.nit): +.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`. test_test_nitunit.nit:36,2--40,4: ERROR: test_foo1 (in file .nitunit/gen_test_test_nitunit.nit): Runtime error: Assert failed (test_test_nitunit.nit:39) @@ -10,9 +12,9 @@ Entities: 27; Documented ones: 3; With nitunits: 3; Failures: 2 TestSuites: Class suites: 1; Test Cases: 3; Failures: 1 assert true -assert false -assert undefined_identifier -outoutout \ No newline at end of file +Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5) +assert false +.nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`. +assert undefined_identifier +Runtime error: Assert failed (test_test_nitunit.nit:39) + \ No newline at end of file diff --git a/tests/sav/nitunit_args6.res b/tests/sav/nitunit_args6.res index 34d34af..2a1e91e 100644 --- a/tests/sav/nitunit_args6.res +++ b/tests/sav/nitunit_args6.res @@ -1,5 +1,6 @@ test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\]. To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). -test_nitunit3/README.md:1,0--13,0: ERROR: nitunit.test_nitunit3>. (in .nitunit/test_nitunit3-0.nit): Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7) +test_nitunit3/README.md:1,0--13,0: ERROR: nitunit.test_nitunit3>. (in .nitunit/test_nitunit3-0.nit): Runtime error +Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7) DocUnits: Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2 @@ -7,8 +8,8 @@ Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2 TestSuites: No test cases found Class suites: 0; Test Cases: 0; Failures: 0 -assert false +Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7) +assert false assert true -assert true +assert true \ No newline at end of file diff --git a/tests/sav/nitunit_args7.res b/tests/sav/nitunit_args7.res index fc20a76..436061a 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:1,0--15,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 +Runtime 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_args9.res b/tests/sav/nitunit_args9.res index de8f951..9cbce91 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 -outBefore Test Tested method After Test Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31) -"> \ No newline at end of file +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 + \ No newline at end of file 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