Merge: lib/github: use serialization
authorJean Privat <jean@pryen.org>
Mon, 15 Aug 2016 17:17:49 +0000 (13:17 -0400)
committerJean Privat <jean@pryen.org>
Mon, 15 Aug 2016 17:17:49 +0000 (13:17 -0400)
Just to see what I broke...

Pull-Request: #2246
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>

27 files changed:
contrib/shibuqam/examples/reloadgame.nit [new file with mode: 0644]
contrib/shibuqam/package.ini [new file with mode: 0644]
contrib/shibuqam/shibuqam.nit [new file with mode: 0644]
examples/calculator/Makefile
examples/calculator/android/res/values/color.xml [new file with mode: 0644]
examples/calculator/org.nitlanguage.scientific_calculator.txt [new file with mode: 0644]
examples/calculator/package.ini
examples/calculator/src/android14.nit [new file with mode: 0644]
examples/calculator/src/android21/android/res/values/styles.xml [new file with mode: 0644]
examples/calculator/src/android21/android21.nit [new file with mode: 0644]
examples/calculator/src/android_calculator.nit [deleted file]
lib/core/file.nit
lib/json/serialization.nit
lib/nitcorn/nitcorn.nit
lib/nitcorn/reactor.nit
lib/nitcorn/server_config.nit
lib/serialization/serialization.nit
src/nitweb.nit
src/web/api_catalog.nit
src/web/api_docdown.nit
src/web/api_feedback.nit
src/web/api_graph.nit
src/web/api_metrics.nit
src/web/api_model.nit
src/web/web_base.nit
tests/sav/nitweb.res
wallet [new file with mode: 0755]

diff --git a/contrib/shibuqam/examples/reloadgame.nit b/contrib/shibuqam/examples/reloadgame.nit
new file mode 100644 (file)
index 0000000..24eecf5
--- /dev/null
@@ -0,0 +1,115 @@
+# 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.
+
+# Example that uses `shibuqam` to authenticate users and count the number of time they reload.
+module reloadgame
+
+import popcorn
+import counter
+import shibuqam
+
+redef class User
+       # How many reload?
+       var seen = 0
+end
+
+# Ugly global class to track the knowledge.
+class DB
+       # All known users
+       var users = new HashMap[String, User]
+end
+# Ugly global instance to track the knowledge.
+fun db: DB do return once new DB
+
+redef class HttpRequest
+       # Like `user` but reuse an user if already seen
+       var reuser: nullable User is lazy do
+               var user = self.user
+               if user == null then return null
+
+               var saved = db.users.get_or_null(user.id)
+               if saved != null then return saved
+
+               db.users[user.id] = user
+               return user
+       end
+end
+
+# The only handler of the example.
+class ReloadGame
+       super Handler
+
+       redef fun get(http_request, response)
+       do
+               var body = """
+                       <!DOCTYPE html>
+                       <head>
+                       <meta charset="utf-8">
+                       <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+                       <title>Nitcorn on Shibboleth/UQAM</title>
+                       </head>
+                       <body>
+                       <div class="container">
+                       <h1>Nitcorn on Shibboleth/UQAM</h1>
+               """
+
+               var user = http_request.user
+
+               if user != null then
+                       user.seen += 1
+
+                       body += """
+                       <p>Welcome {{{user.given_name}}}</p>
+                       <ul>
+                       <li>Full Name: {{{user.display_name}}}</li>
+                       <li>E-Mail: {{{user.email}}}</li>
+                       <li>Id: {{{user.id}}}</li>
+                       <li>Score: {{{user.seen}}}</li>
+                       </ul>
+                       """
+
+                       #for k, v in http_request.header do body += "<li>{k}: {v}</li>"
+               else
+                       # The login page, at the location the reverse proxy is expected to be configured
+                       # to force an authentication.
+                       var login = "/securep/login"
+                       body += """
+                       <p>Welcome annonymous, please <a href="{{{login}}}">log in</a>.</p>
+                       """
+               end
+
+               var score = new Counter[User]
+               for u in db.users.values do
+                       score[u] = u.seen
+               end
+
+               body += "<h2>Scoreboard</h2><ul>"
+               for u in score.sort.reversed do
+                       body += "<li><img src='{u.avatar}'> {u.display_name}: {u.seen}</li>"
+               end
+
+
+               body += """</ul>
+                       </div>
+                       </body>
+                       </html>
+               """
+
+               response.html body
+       end
+end
+
+var app = new App
+app.use("/*", new ReloadGame)
+app.listen("localhost", 3000)
diff --git a/contrib/shibuqam/package.ini b/contrib/shibuqam/package.ini
new file mode 100644 (file)
index 0000000..8c619ad
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=shibuqam
+tags=web
+maintainer=Jean Privat <jean@pryen.org>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/contrib/shibuqam/
+git=https://github.com/nitlang/nit.git
+git.directory=contrib/shibuqam/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/contrib/shibuqam/shibuqam.nit b/contrib/shibuqam/shibuqam.nit
new file mode 100644 (file)
index 0000000..e8da9a5
--- /dev/null
@@ -0,0 +1,69 @@
+# 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.
+
+# Gather the authenticated users on UQAM websites.
+#
+# The main method to use is `HttpRequest::user` and it extracts the information
+# of the authenticated user from the request header.
+# The real authentication must be done by a mandatory reverse proxy server.
+module shibuqam
+
+import nitcorn
+private import md5
+
+# Information on a user from Shibboleth/UQAM
+class User
+       # The *code permanent* (or the uid for non student)
+       var id: String
+
+       # Usually the first name
+       var given_name: String
+
+       # Usually "FamilyName, FirstName"
+       var display_name: String
+
+       # The email @courrier.uqam.ca (or @uqam.ca for non student)
+       var email: String
+
+       # The Gravatar URL (based on `email`)
+       var avatar: String is lazy do
+               var md5 = email.md5
+               return "https://www.gravatar.com/avatar/{md5}?d=retro"
+       end
+end
+
+redef class HttpRequest
+       # Extract the Shibboleth/UQAM information from the header, if any.
+       #
+       # We assume that a reverse proxy does the authentication and fill the request header.
+       # If the server is accessible directly, these headers can be easily forged.
+       # Thus, we assume that the reverse proxy is not by-passable.
+       #
+       # The reverse proxy might choose to force an authentication or not.
+       # If there is no authentication, there is no information in the request header (or with the `(null)` value).
+       # In this case, `null` is returned by this function.
+       fun user: nullable User do
+               var user = header.get_or_null("Remote-User")
+               if user == null or user == "(null)" then return null
+
+               var display_name = header.get_or_null("User-Display-Name")
+               var given_name = header.get_or_null("User-Given-Name")
+               var email = header.get_or_null("User-Mail")
+
+               if display_name == null or given_name == null or email == null then return null
+
+               var res = new User(user, given_name, display_name, email)
+               return res
+       end
+end
index bf2a19e..21d74f8 100644 (file)
@@ -13,34 +13,49 @@ bin/scientific: $(shell ${NITLS} -M scientific linux) ${NITC}
 
 # ---
 # Android
+#
+# There are 4 versions, combining 2 variations:
+# * scientific vs non-scientific
+# * android API 21+ vs under 21
 
-android: bin/calculator.apk bin/scientific.apk
+android: bin/calculator14.apk bin/scientific14.apk bin/calculator21.apk bin/scientific21.apk
 
-bin/calculator.apk: $(shell ${NITLS} -M src/android_calculator.nit) ${NITC} android/res/drawable-hdpi/icon.png
+bin/calculator14.apk: $(shell ${NITLS} -M src/android14.nit) ${NITC} android/res/drawable-hdpi/icon.png
        mkdir -p bin
-       ${NITC} -o $@ src/android_calculator.nit -D debug
+       ${NITC} -o $@ src/android14.nit -D debug
 
-bin/scientific.apk: $(shell ${NITLS} -M src/scientific src/android_calculator.nit) ${NITC} src/scientific/android/res/drawable-hdpi/icon.png
+bin/calculator21.apk: $(shell ${NITLS} -M src/android21) ${NITC} android/res/drawable-hdpi/icon.png
        mkdir -p bin
-       ${NITC} -o $@ src/scientific -m src/android_calculator.nit -D debug
+       ${NITC} -o $@ src/android21 -D debug
 
-android-release: $(shell ${NITLS} -M src/scientific src/android_calculator.nit) ${NITC} android/res/drawable-hdpi/icon.png
+bin/scientific14.apk: $(shell ${NITLS} -M src/scientific src/android14.nit) ${NITC} src/scientific/android/res/drawable-hdpi/icon.png
        mkdir -p bin
-       ${NITC} -o bin/calculator.apk src/scientific -m src/android_calculator.nit --release
+       ${NITC} -o $@ src/scientific -m src/android14.nit -D debug
+
+bin/scientific21.apk: $(shell ${NITLS} -M src/scientific src/android21) ${NITC} src/scientific/android/res/drawable-hdpi/icon.png
+       mkdir -p bin
+       ${NITC} -o $@ src/scientific -m src/android21 -D debug
+
+android-release: $(shell ${NITLS} -M src/scientific src/android14.nit) ${NITC} android/res/drawable-hdpi/icon.png
+       mkdir -p bin
+       ${NITC} -o bin/calculator14.apk src/android14.nit --release
+       ${NITC} -o bin/calculator21.apk src/android21 --release
+       ${NITC} -o bin/scientific14.apk src/scientific -m src/android14.nit --release
+       ${NITC} -o bin/scientific21.apk src/scientific -m src/android21 --release
 
 android/res/drawable-hdpi/icon.png: art/icon.svg ../../contrib/inkscape_tools/bin/svg_to_icons
        mkdir -p android/res
        ../../contrib/inkscape_tools/bin/svg_to_icons art/icon.svg --android --out android/res/
 
-src/scientific/android/res/drawable-hdpi/icon.png: art/icon_sci.svg ../../contrib/inkscape_tools/bin/svg_to_icons
+src/scientific/android/res/drawable-hdpi/icon.png: art/icon-sci.svg ../../contrib/inkscape_tools/bin/svg_to_icons
        mkdir -p src/scientific/android/res
        ../../contrib/inkscape_tools/bin/svg_to_icons art/icon-sci.svg --android --out src/scientific/android/res/
 
 ../../contrib/inkscape_tools/bin/svg_to_icons:
        make -C ../../contrib/inkscape_tools/
 
-android-install: bin/calculator.apk
-       adb install -r bin/calculator.apk
+android-install: bin/calculator14.apk
+       adb install -r bin/calculator14.apk
 
 # ---
 # iOS
diff --git a/examples/calculator/android/res/values/color.xml b/examples/calculator/android/res/values/color.xml
new file mode 100644 (file)
index 0000000..3ed5b06
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+
+    <!-- Default background color for the status bar. -->
+    <color name="calculator_accent_color">#00BCD4</color>
+
+    <!-- Color to indicate an error has occured. -->
+    <color name="calculator_error_color">#F40056</color>
+
+    <!-- Background color of the calculator display. -->
+    <color name="display_background_color">#FFF</color>
+
+    <!-- Text color for the formula in the calculator display. -->
+    <color name="display_formula_text_color">#8A000000</color>
+
+    <!-- Text color for the result in the calculator display. -->
+    <color name="display_result_text_color">#6C000000</color>
+
+    <!-- Background color for the numeric pad. -->
+    <color name="pad_numeric_background_color">#434343</color>
+
+    <!-- Background color for the operator pad. -->
+    <color name="pad_operator_background_color">#636363</color>
+
+    <!-- Background color for the advanced pad. -->
+    <color name="pad_advanced_background_color">#1DE9B6</color>
+
+    <!-- Text color for a button in a pad. -->
+    <color name="pad_button_text_color">#FFF</color>
+
+    <!-- Text color for a button in the advanced pad. -->
+    <color name="pad_button_advanced_text_color">#91000000</color>
+
+    <!-- Ripple color when a button is pressed in a pad. -->
+    <color name="pad_button_ripple_color">#33FFFFFF</color>
+
+    <!-- Ripple color when a button is pressed in a pad. -->
+    <color name="pad_button_advanced_ripple_color">#1A000000</color>
+
+</resources>
diff --git a/examples/calculator/org.nitlanguage.scientific_calculator.txt b/examples/calculator/org.nitlanguage.scientific_calculator.txt
new file mode 100644 (file)
index 0000000..0ec8577
--- /dev/null
@@ -0,0 +1,10 @@
+Categories:Nit
+License:Apache2
+Web Site:http://nitlanguage.org
+Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/examples/calculator
+Issue Tracker:https://github.com/nitlang/nit/issues
+
+Summary:A Scientific Calculator
+Description:
+10 digits, 16 operations, hours of fun.
+.
index 8237327..014014d 100644 (file)
@@ -9,4 +9,4 @@ git=https://github.com/nitlang/nit.git
 git.directory=examples/calculator/
 homepage=http://nitlanguage.org
 issues=https://github.com/nitlang/nit/issues
-apk=http://nitlanguage.org/fdroid/apk/calculator.apk
+apk=http://nitlanguage.org/fdroid/apk/calculator21.apk
diff --git a/examples/calculator/src/android14.nit b/examples/calculator/src/android14.nit
new file mode 100644 (file)
index 0000000..8695c3b
--- /dev/null
@@ -0,0 +1,64 @@
+# 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.
+
+# Aesthetic adaptations for Android for API 14+
+module android14
+
+import calculator
+import android
+
+redef class Button
+       init do set_android_style(native, app.native_activity,
+                                 (text or else "?").is_int,
+                                 ["+","-","×","C","÷","=","."].has(text))
+
+       # Set color and text style
+       private fun set_android_style(java_button: NativeButton, activity: NativeActivity,
+               is_number: Bool, is_basic_op: Bool)
+       in "Java" `{
+               // Set color
+               int back_color_id = 0;
+               if (is_number)
+                       back_color_id = R.color.pad_numeric_background_color;
+               else if (is_basic_op)
+                       back_color_id = R.color.pad_operator_background_color;
+               else {
+                       back_color_id = R.color.pad_advanced_background_color;
+
+                       int text_color = activity.getResources().getColor(R.color.pad_button_advanced_text_color);
+                       java_button.setTextColor(text_color);
+               }
+               java_button.setBackgroundResource(back_color_id);
+
+               // Center label, use lowercase and make text bigger
+               java_button.setGravity(android.view.Gravity.CENTER);
+               java_button.setAllCaps(false);
+               java_button.setTextSize(android.util.TypedValue.COMPLEX_UNIT_FRACTION, 100.0f);
+       `}
+end
+
+redef class TextInput
+       init do set_android_style(native, app.native_activity)
+
+       # Set text style and hide cursor
+       private fun set_android_style(java_edit_text: NativeEditText, activity: NativeActivity)
+       in "Java" `{
+               java_edit_text.setBackgroundResource(R.color.display_background_color);
+               java_edit_text.setTextColor(
+                       activity.getResources().getColor(R.color.display_formula_text_color));
+               java_edit_text.setTextSize(android.util.TypedValue.COMPLEX_UNIT_FRACTION, 120.0f);
+               java_edit_text.setCursorVisible(false);
+               java_edit_text.setGravity(android.view.Gravity.CENTER_VERTICAL | android.view.Gravity.END);
+       `}
+end
diff --git a/examples/calculator/src/android21/android/res/values/styles.xml b/examples/calculator/src/android21/android/res/values/styles.xml
new file mode 100644 (file)
index 0000000..95c844a
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="CalculatorTheme" parent="@android:style/Theme.Material.Light.NoActionBar">
+        <item name="android:colorPrimary">@color/calculator_accent_color</item>
+        <item name="android:navigationBarColor">@color/calculator_accent_color</item>
+        <item name="android:statusBarColor">@color/calculator_accent_color</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>
+
+    <style name="DisplayEditTextStyle" parent="@android:style/Widget.Material.Light.EditText">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:cursorVisible">false</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:gravity">bottom|end</item>
+    </style>
+
+    <style name="PadButtonStyle" parent="@android:style/Widget.Material.Light.Button.Borderless">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:gravity">center</item>
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:minWidth">0dip</item>
+        <item name="android:minHeight">0dip</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:textColor">@color/pad_button_text_color</item>
+    </style>
+
+    <style name="PadLayoutStyle">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+    </style>
+</resources>
diff --git a/examples/calculator/src/android21/android21.nit b/examples/calculator/src/android21/android21.nit
new file mode 100644 (file)
index 0000000..36d1025
--- /dev/null
@@ -0,0 +1,50 @@
+# 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.
+
+# Aesthetic adaptations for Android Lollypop (API 21)
+module android21 is
+       android_api_min 21
+       android_api_target 21
+       android_manifest_activity """android:theme="@style/CalculatorTheme" """
+       app_files
+end
+
+import android14
+
+redef class TextInput
+       init do
+               set_android_style(native, app.native_activity)
+               super
+       end
+
+       # Deactivate the virtual keyboard and set the text style from XML resources
+       private fun set_android_style(java_edit_text: NativeEditText, activity: NativeActivity)
+       in "Java" `{
+               java_edit_text.setShowSoftInputOnFocus(false);
+               java_edit_text.setTextAppearance(activity, R.style.DisplayEditTextStyle);
+       `}
+end
+
+redef class Button
+       init do
+               set_text_style(native, app.native_activity)
+               super
+       end
+
+       # Set the text style from XML resources
+       private fun set_text_style(java_button: NativeButton, activity: NativeActivity)
+       in "Java" `{
+               java_button.setTextAppearance(activity, R.style.PadButtonStyle);
+       `}
+end
diff --git a/examples/calculator/src/android_calculator.nit b/examples/calculator/src/android_calculator.nit
deleted file mode 100644 (file)
index 1d51073..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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.
-
-# Aesthetic adaptations for Android
-module android_calculator
-
-import calculator
-import android
-
-redef class Button
-       init do set_android_style(native, (text or else "?").is_int)
-
-       private fun set_android_style(java_button: NativeButton, is_number: Bool)
-       in "Java" `{
-               // Flatten the background and use a different color for digit buttons
-               int color = is_number? android.graphics.Color.DKGRAY: android.graphics.Color.TRANSPARENT;
-               java_button.setBackgroundColor(color);
-
-               // Center the label on both horizontal and vertical axes
-               java_button.setGravity(android.view.Gravity.CENTER);
-
-               // Set lowercase text to correctly display constants like e and Ï€
-               java_button.setAllCaps(false);
-       `}
-end
index bea9672..79ddf45 100644 (file)
@@ -626,6 +626,22 @@ class Path
                return res
        end
 
+       # Correctly join `self` with `subpath` using the directory separator.
+       #
+       # Using a standard "{self}/{path}" does not work in the following cases:
+       #
+       # * `self` is empty.
+       # * `path` starts with `'/'`.
+       #
+       # This method ensures that the join is valid.
+       #
+       #     var hello = "hello".to_path
+       #     assert (hello/"world").to_s   == "hello/world"
+       #     assert ("hel/lo".to_path / "wor/ld").to_s == "hel/lo/wor/ld"
+       #     assert ("".to_path / "world").to_s == "world"
+       #     assert (hello / "/world").to_s  == "/world"
+       #     assert ("hello/".to_path / "world").to_s  == "hello/world"
+       fun /(subpath: String): Path do return new Path(path / subpath)
 
        # Lists the files contained within the directory at `path`.
        #
@@ -657,7 +673,7 @@ class Path
                        end
                        var name = de.to_s_with_copy
                        if name == "." or name == ".." then continue
-                       res.add new Path(path / name)
+                       res.add self / name
                end
                d.closedir
 
index 6a66407..e2c3adf 100644 (file)
@@ -242,6 +242,9 @@ class JsonDeserializer
        # Depth-first path in the serialized object tree.
        private var path = new Array[Map[String, nullable Object]]
 
+       # Names of the attributes from the root to the object currently being deserialized
+       var attributes_path = new Array[String]
+
        # Last encountered object reference id.
        #
        # See `id_to_object`.
@@ -265,7 +268,10 @@ class JsonDeserializer
 
                var value = current[name]
 
-               return convert_object(value)
+               attributes_path.add name
+               var res = convert_object(value)
+               attributes_path.pop
+               return res
        end
 
        # This may be called multiple times by the same object from constructors
@@ -441,11 +447,16 @@ class JsonDeserializer
                return convert_object(root)
        end
 
-       # User customizable heuristic to get the name of the Nit class to deserialize `json_object`
+       # User customizable heuristic to infer the name of the Nit class to deserialize `json_object`
        #
        # This method is called only when deserializing an object without the metadata `__class`.
-       # Return the class name as a `String` when it can be inferred.
-       # Return `null` when the class name cannot be found.
+       # Use the content of `json_object` to identify what Nit class it should be deserialized into.
+       # Or use `self.attributes_path` indicating where the deserialized object will be stored,
+       # is is less reliable as some objects don't have an associated attribute:
+       # the root/first deserialized object and collection elements.
+       #
+       # Return the class name as a `String` when it can be inferred,
+       # or `null` when the class name cannot be found.
        #
        # If a valid class name is returned, `json_object` will then be deserialized normally.
        # So it must contain the attributes of the corresponding class, as usual.
@@ -461,6 +472,7 @@ class JsonDeserializer
        #     serialize
        #
        #     var error: String
+       #     var related_data: MyData
        # end
        #
        # class MyJsonDeserializer
@@ -468,18 +480,26 @@ class JsonDeserializer
        #
        #     redef fun class_name_heuristic(json_object)
        #     do
+       #         # Infer the Nit class from the content of the JSON object.
        #         if json_object.keys.has("error") then return "MyError"
        #         if json_object.keys.has("data") then return "MyData"
+       #
+       #         # Infer the Nit class from the attribute where it will be stored.
+       #         # This line duplicates a previous line, and would only apply when
+       #         # `MyData` is within a `MyError`.
+       #         if attributes_path.not_empty and attributes_path.last == "related_data" then return "MyData"
+       #
        #         return null
        #     end
        # end
        #
-       # var json = """{"data": "some other data"}"""
+       # var json = """{"data": "some data"}"""
        # var deserializer = new MyJsonDeserializer(json)
        # var deserialized = deserializer.deserialize
        # assert deserialized isa MyData
        #
-       # json = """{"error": "some error message"}"""
+       # json = """{"error": "some error message",
+       #            "related_data": {"data": "some other data"}"""
        # deserializer = new MyJsonDeserializer(json)
        # deserialized = deserializer.deserialize
        # assert deserialized isa MyError
index 2a5b43a..40d02ef 100644 (file)
 # Basic usage example:
 # ~~~~
 # class MyAction
-#      super Action
-#
-#      redef fun answer(http_request, turi)
-#      do
-#              var response = new HttpResponse(200)
-#              response.body = """
-#              <!DOCTYPE html>
-#              <head>
-#                      <meta charset="utf-8">
-#                      <title>Hello World</title>
-#              </head>
-#              <body>
-#                      <p>Hello World</p>
-#              </body>
-#              </html>"""
-#              return response
-#      end
+#     super Action
+#
+#     redef fun answer(http_request, turi)
+#     do
+#         var response = new HttpResponse(200)
+#         response.body = """
+# <!DOCTYPE html>
+# <head>
+#     <meta charset="utf-8">
+#     <title>Hello World</title>
+# </head>
+# <body>
+#     <p>Hello World</p>
+# </body>
+# </html>"""
+#         return response
+#     end
 # end
 #
-# var vh = new VirtualHost("localhost:80")
+# # Listen to port 8080 on all interfaces
+# var vh = new VirtualHost("0.0.0.0:8080")
 #
 # # Serve index.html with our custom handler
 # vh.routes.add new Route("/index.html", new MyAction)
index de9ddc0..66334c3 100644 (file)
@@ -201,8 +201,15 @@ redef class Interfaces
        redef fun add(e)
        do
                super
-               var config = vh.server_config
-               if config != null then sys.listen_on(e, config.factory)
+               var config = virtual_host.server_config
+               if config != null then register_and_listen(e, config)
+       end
+
+       # Indirection to `listen_on` and check if this targets all addresses
+       private fun register_and_listen(e: Interface, config: ServerConfig)
+       do
+               listen_on(e, config.factory)
+               if e.name == "0.0.0.0" or e.name == "::0" then config.default_virtual_host = virtual_host
        end
 
        # TODO remove
@@ -212,7 +219,7 @@ redef class VirtualHosts
        redef fun add(e)
        do
                super
-               for i in e.interfaces do sys.listen_on(i, config.factory)
+               for i in e.interfaces do e.interfaces.register_and_listen(i, config)
        end
 
        # TODO remove
index e61caad..3cf51ff 100644 (file)
@@ -25,7 +25,7 @@ class ServerConfig
        var virtual_hosts = new VirtualHosts(self)
 
        # Default `VirtualHost` to respond to requests not handled by any of the `virtual_hosts`
-       var default_virtual_host: nullable VirtualHost = null
+       var default_virtual_host: nullable VirtualHost = null is writable
 end
 
 # A `VirtualHost` configuration
@@ -82,7 +82,7 @@ class Interfaces
        super Array[Interface]
 
        # Back reference to the associtated `VirtualHost`
-       var vh: VirtualHost
+       var virtual_host: VirtualHost
 
        # Add an `Interface` described by `text` formatted as `interface.name.com:port`
        fun add_from_string(text: String)
index 0ad3019..435f8df 100644 (file)
@@ -88,21 +88,27 @@ end
 
 # Abstract deserialization service
 #
-# After initialization of one of its sub-classes, call `deserialize`
+# The main service is `deserialize`.
 abstract class Deserializer
-       # Main method of this class, returns a Nit object
+       # Deserialize and return an object, storing errors in the attribute `errors`
+       #
+       # This method behavior varies according to the implementation engines.
        fun deserialize: nullable Object is abstract
 
-       # Internal method to be implemented by sub-classes
+       # Deserialize the attribute with `name` from the object open for deserialization
+       #
+       # Internal method to be implemented by the engines.
        fun deserialize_attribute(name: String): nullable Object is abstract
 
-       # Internal method called by objects in creation,
-       # to be implemented by sub-classes
+       # Register a newly allocated object (even if not completely built)
+       #
+       # Internal method called by objects in creation, to be implemented by the engines.
        fun notify_of_creation(new_object: Object) is abstract
 
        # Deserialize the next available object as an instance of `class_name`
        #
-       # Returns the deserialized object on success, aborts on error.
+       # Return the deserialized object on success and
+       # record in `errors` if `class_name` is unknown.
        #
        # This method should be redefined for each custom subclass of `Serializable`.
        # All refinement should look for a precise `class_name` and call super
index 7c6cdc1..9376172 100644 (file)
 # Runs a webserver based on nitcorn that render things from model.
 module nitweb
 
+import popcorn::pop_config
 import frontend
 import web
 
 redef class ToolContext
 
-       # Host name to bind on.
+       # Path to app config file.
+       var opt_config = new OptionString("Path to app config file", "--config")
+
+       # Host name to bind on (will overwrite the config one).
        var opt_host = new OptionString("Host to bind the server on", "--host")
 
-       # Port number to bind on.
-       var opt_port = new OptionInt("Port number to use", 3000, "--port")
+       # Port number to bind on (will overwrite the config one).
+       var opt_port = new OptionInt("Port number to use", -1, "--port")
 
        # Web rendering phase.
        var webphase: Phase = new NitwebPhase(self, null)
 
        init do
                super
-               option_context.add_option(opt_host, opt_port)
+               option_context.add_option(opt_config, opt_host, opt_port)
        end
 end
 
 # Phase that builds the model and wait for http request to serve pages.
 private class NitwebPhase
        super Phase
-       redef fun process_mainmodule(mainmodule, mmodules)
-       do
-               var model = mainmodule.model
-               var modelbuilder = toolcontext.modelbuilder
 
-               # Build catalog
+       # Build the nitweb config from `toolcontext` options.
+       fun build_config(toolcontext: ToolContext, mainmodule: MModule): NitwebConfig do
+               var config_file = toolcontext.opt_config.value
+               if config_file == null then config_file = "nitweb.ini"
+               var config = new NitwebConfig(
+                       config_file,
+                       toolcontext.modelbuilder.model,
+                       mainmodule,
+                       toolcontext.modelbuilder)
+               var opt_host = toolcontext.opt_host.value
+               if opt_host != null then config["app.host"] = opt_host
+               var opt_port = toolcontext.opt_port.value
+               if opt_port >= 0 then config["app.port"] = opt_port.to_s
+               return config
+       end
+
+       # Build the nit catalog used in homepage.
+       fun build_catalog(model: Model, modelbuilder: ModelBuilder): Catalog do
                var catalog = new Catalog(modelbuilder)
                for mpackage in model.mpackages do
                        catalog.deps.add_node(mpackage)
@@ -59,61 +76,49 @@ private class NitwebPhase
                        catalog.git_info(mpackage)
                        catalog.package_page(mpackage)
                end
+               return catalog
+       end
 
-               # Prepare mongo connection
-               var mongo = new MongoClient("mongodb://localhost:27017/")
-               var db = mongo.database("nitweb")
-               var collection = db.collection("stars")
-
-               # Run the server
-               var host = toolcontext.opt_host.value or else "localhost"
-               var port = toolcontext.opt_port.value
+       redef fun process_mainmodule(mainmodule, mmodules)
+       do
+               var model = mainmodule.model
+               var modelbuilder = toolcontext.modelbuilder
+               var config = build_config(toolcontext, mainmodule)
+               var catalog = build_catalog(model, modelbuilder)
 
                var app = new App
 
                app.use_before("/*", new RequestClock)
-               app.use("/api", new APIRouter(model, modelbuilder, mainmodule, catalog, collection))
+               app.use("/api", new NitwebAPIRouter(config, catalog))
                app.use("/*", new StaticHandler(toolcontext.share_dir / "nitweb", "index.html"))
                app.use_after("/*", new ConsoleLog)
 
-               app.listen(host, port.to_i)
+               app.listen(config.app_host, config.app_port)
        end
 end
 
 # Group all api handlers in one router.
-class APIRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # ModelBuilder to pass to handlers.
-       var modelbuilder: ModelBuilder
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+class NitwebAPIRouter
+       super APIRouter
 
        # Catalog to pass to handlers.
        var catalog: Catalog
 
-       # Mongo collection used to store ratings.
-       var collection: MongoCollection
-
        init do
-               use("/catalog", new APICatalogRouter(model, mainmodule, catalog))
-               use("/list", new APIList(model, mainmodule))
-               use("/search", new APISearch(model, mainmodule))
-               use("/random", new APIRandom(model, mainmodule))
-               use("/entity/:id", new APIEntity(model, mainmodule))
-               use("/code/:id", new APIEntityCode(model, mainmodule, modelbuilder))
-               use("/uml/:id", new APIEntityUML(model, mainmodule))
-               use("/linearization/:id", new APIEntityLinearization(model, mainmodule))
-               use("/defs/:id", new APIEntityDefs(model, mainmodule))
-               use("/feedback/", new APIFeedbackRouter(model, mainmodule, collection))
-               use("/inheritance/:id", new APIEntityInheritance(model, mainmodule))
-               use("/graph/", new APIGraphRouter(model, mainmodule))
-               use("/docdown/", new APIDocdown(model, mainmodule, modelbuilder))
-               use("/metrics/", new APIMetricsRouter(model, mainmodule))
+               use("/catalog", new APICatalogRouter(config, catalog))
+               use("/list", new APIList(config))
+               use("/search", new APISearch(config))
+               use("/random", new APIRandom(config))
+               use("/entity/:id", new APIEntity(config))
+               use("/code/:id", new APIEntityCode(config))
+               use("/uml/:id", new APIEntityUML(config))
+               use("/linearization/:id", new APIEntityLinearization(config))
+               use("/defs/:id", new APIEntityDefs(config))
+               use("/feedback/", new APIFeedbackRouter(config))
+               use("/inheritance/:id", new APIEntityInheritance(config))
+               use("/graph/", new APIGraphRouter(config))
+               use("/docdown/", new APIDocdown(config))
+               use("/metrics/", new APIMetricsRouter(config))
        end
 end
 
@@ -121,7 +126,7 @@ end
 var toolcontext = new ToolContext
 var tpl = new Template
 tpl.add "Usage: nitweb [OPTION]... <file.nit>...\n"
-tpl.add "Run a webserver based on nitcorn that serve pages about model."
+tpl.add "Run a webserver based on nitcorn that serves pages about model."
 toolcontext.tooldescription = tpl.write_to_string
 
 # process options
index 72d207f..f0c70c6 100644 (file)
@@ -19,23 +19,17 @@ import catalog
 
 # Group all api handlers in one router.
 class APICatalogRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        # Catalog to pass to handlers.
        var catalog: Catalog
 
        init do
-               use("/highlighted", new APICatalogHighLighted(model, mainmodule, catalog))
-               use("/required", new APICatalogMostRequired(model, mainmodule, catalog))
-               use("/bytags", new APICatalogByTags(model, mainmodule, catalog))
-               use("/contributors", new APICatalogContributors(model, mainmodule, catalog))
-               use("/stats", new APICatalogStats(model, mainmodule, catalog))
+               use("/highlighted", new APICatalogHighLighted(config, catalog))
+               use("/required", new APICatalogMostRequired(config, catalog))
+               use("/bytags", new APICatalogByTags(config, catalog))
+               use("/contributors", new APICatalogContributors(config, catalog))
+               use("/stats", new APICatalogStats(config, catalog))
        end
 end
 
@@ -74,7 +68,7 @@ class APICatalogStats
 
        redef fun get(req, res) do
                var obj = new JsonObject
-               obj["packages"] = model.mpackages.length
+               obj["packages"] = config.model.mpackages.length
                obj["maintainers"] = catalog.maint2proj.length
                obj["contributors"] = catalog.contrib2proj.length
                obj["modules"] = catalog.mmodules.sum
@@ -97,7 +91,7 @@ class APICatalogMostRequired
        redef fun get(req, res) do
                if catalog.deps.not_empty then
                        var reqs = new Counter[MPackage]
-                       for p in model.mpackages do
+                       for p in config.model.mpackages do
                                reqs[p] = catalog.deps[p].smallers.length - 1
                        end
                        res.json list_best(reqs)
index 3510b64..bf4c80f 100644 (file)
@@ -24,13 +24,10 @@ import doc_commands
 class APIDocdown
        super APIHandler
 
-       # Modelbuilder used by the commands
-       var modelbuilder: ModelBuilder
-
        # Specific Markdown processor to use within Nitweb
        var md_processor: MarkdownProcessor is lazy do
                var proc = new MarkdownProcessor
-               proc.emitter.decorator = new NitwebDecorator(view, modelbuilder)
+               proc.emitter.decorator = new NitwebDecorator(view, config.modelbuilder)
                return proc
        end
 
index 61ba756..76e9d67 100644 (file)
@@ -18,21 +18,38 @@ module api_feedback
 import web_base
 import mongodb
 
-# Group all api handlers in one router
-class APIFeedbackRouter
-       super Router
+redef class NitwebConfig
+
+       # MongoDB uri used for data persistence.
+       #
+       # * key: `mongo.uri`
+       # * default: `mongodb://localhost:27017/`
+       var mongo_uri: String is lazy do
+               return value_or_default("mongo.uri", "mongodb://localhost:27017/")
+       end
 
-       # Model to pass to handlers
-       var model: Model
+       # MongoDB DB used for data persistence.
+       #
+       # * key: `mongo.db`
+       # * default: `nitweb`
+       var mongo_db: String is lazy do return value_or_default("mongo.db", "nitweb")
 
-       # Mainmodule to pass to handlers
-       var mainmodule: MModule
+       # Mongo instance
+       var mongo: MongoClient is lazy do return new MongoClient(mongo_uri)
 
-       # Mongo collection used to store ratings
-       var collection: MongoCollection
+       # Database instance
+       var db: MongoDb is lazy do return mongo.database(mongo_db)
+
+       # MongoDB collection used to store stars.
+       var stars: MongoCollection is lazy do return db.collection("stars")
+end
+
+# Group all api handlers in one router
+class APIFeedbackRouter
+       super APIRouter
 
        init do
-               use("/stars/:id", new APIStars(model, mainmodule, collection))
+               use("/stars/:id", new APIStars(config))
        end
 end
 
@@ -40,9 +57,6 @@ end
 class APIStars
        super APIHandler
 
-       # Collection used to store ratings
-       var collection: MongoCollection
-
        redef fun get(req, res) do
                var mentity = mentity_from_uri(req, res)
                if mentity == null then
@@ -71,7 +85,7 @@ class APIStars
                end
 
                var val = new MEntityRating(mentity.full_name, rating, get_time)
-               collection.insert(val.json)
+               config.stars.insert(val.json)
 
                res.json mentity_ratings(mentity)
        end
@@ -82,7 +96,7 @@ class APIStars
 
                var req = new JsonObject
                req["mentity"] = mentity.full_name
-               var rs = collection.find_all(req)
+               var rs = config.stars.find_all(req)
                for r in rs do ratings.ratings.add new MEntityRating.from_json(r)
                return ratings
        end
index 67a32e2..d58031d 100644 (file)
@@ -21,16 +21,10 @@ import uml
 
 # Group all api handlers in one router.
 class APIGraphRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        init do
-               use("/inheritance/:id", new APIInheritanceGraph(model, mainmodule))
+               use("/inheritance/:id", new APIInheritanceGraph(config))
        end
 end
 
index 30a53f1..8d64c04 100644 (file)
@@ -19,16 +19,10 @@ import metrics
 
 # Group all api handlers in one router.
 class APIMetricsRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        init do
-               use("/structural/:id", new APIStructuralMetrics(model, mainmodule))
+               use("/structural/:id", new APIStructuralMetrics(config))
        end
 end
 
@@ -36,6 +30,7 @@ class APIStructuralMetrics
        super APIHandler
 
        private fun mclasses_metrics: MetricSet do
+               var mainmodule = config.mainmodule
                var metrics = new MetricSet
                metrics.register(new CNOA(mainmodule, view))
                metrics.register(new CNOP(mainmodule, view))
@@ -58,6 +53,7 @@ class APIStructuralMetrics
        end
 
        private fun mmodules_metrics: MetricSet do
+               var mainmodule = config.mainmodule
                var metrics = new MetricSet
                metrics.register(new MNOA(mainmodule, view))
                metrics.register(new MNOP(mainmodule, view))
index f8c6eeb..63fd636 100644 (file)
@@ -148,7 +148,7 @@ class APIEntityLinearization
                        res.error 404
                        return
                end
-               var lin = mentity.collect_linearization(mainmodule)
+               var lin = mentity.collect_linearization(config.mainmodule)
                if lin == null then
                        res.error 404
                        return
@@ -206,7 +206,7 @@ class APIEntityUML
                var dot
                if mentity isa MClassDef then mentity = mentity.mclass
                if mentity isa MClass then
-                       var uml = new UMLModel(view, mainmodule)
+                       var uml = new UMLModel(view, config.mainmodule)
                        dot = uml.generate_class_uml.write_to_string
                else if mentity isa MModule then
                        var uml = new UMLModel(view, mentity)
@@ -225,9 +225,6 @@ end
 class APIEntityCode
        super APIHandler
 
-       # Modelbuilder used to access sources.
-       var modelbuilder: ModelBuilder
-
        redef fun get(req, res) do
                var mentity = mentity_from_uri(req, res)
                if mentity == null then return
@@ -241,7 +238,7 @@ class APIEntityCode
 
        # Highlight `mentity` source code.
        private fun render_source(mentity: MEntity): nullable HTMLTag do
-               var node = modelbuilder.mentity2node(mentity)
+               var node = config.modelbuilder.mentity2node(mentity)
                if node == null then return null
                var hl = new HighlightVisitor
                hl.enter_visit node
index 20d0b05..c535936 100644 (file)
@@ -19,10 +19,11 @@ import model::model_views
 import model::model_json
 import doc_down
 import popcorn
+import popcorn::pop_config
 
-# Specific nitcorn Action that uses a Model
-class ModelHandler
-       super Handler
+# Nitweb config file.
+class NitwebConfig
+       super AppConfig
 
        # Model to use.
        var model: Model
@@ -30,6 +31,17 @@ class ModelHandler
        # MModule used to flatten model.
        var mainmodule: MModule
 
+       # Modelbuilder used to access sources.
+       var modelbuilder: ModelBuilder
+end
+
+# Specific nitcorn Action that uses a Model
+class ModelHandler
+       super Handler
+
+       # App config.
+       var config: NitwebConfig
+
        # 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
@@ -38,7 +50,7 @@ class ModelHandler
 
        # Init the model view from the `req` uri parameters.
        fun init_model_view(req: HttpRequest): ModelView do
-               var view = new ModelView(model)
+               var view = new ModelView(config.model)
                var show_private = req.bool_arg("private") or else false
                if not show_private then view.min_visibility = protected_visibility
 
@@ -59,7 +71,7 @@ abstract class APIHandler
        #
        # So we can cache the model view.
        var view: ModelView is lazy do
-               var view = new ModelView(model)
+               var view = new ModelView(config.model)
                view.min_visibility = private_visibility
                view.include_fictive = true
                view.include_empty_doc = true
@@ -87,6 +99,14 @@ abstract class APIHandler
        end
 end
 
+# A Rooter dedicated to APIHandlers.
+class APIRouter
+       super Router
+
+       # App config.
+       var config: NitwebConfig
+end
+
 redef class MEntity
 
        # URL to `self` within the web interface.
index 7c0cf6d..a6f1e98 100644 (file)
@@ -1,3 +1,3 @@
 Usage: nitweb [OPTION]... <file.nit>...
-Run a webserver based on nitcorn that serve pages about model.
+Run a webserver based on nitcorn that serves pages about model.
 Use --help for help
diff --git a/wallet b/wallet
new file mode 100755 (executable)
index 0000000..567234d
Binary files /dev/null and b/wallet differ