Merge: nitweb: search field
authorJean Privat <jean@pryen.org>
Fri, 3 Jun 2016 17:16:57 +0000 (13:16 -0400)
committerJean Privat <jean@pryen.org>
Fri, 3 Jun 2016 17:16:57 +0000 (13:16 -0400)
* Introduce a search field in the top navbar connected to the `/search` service.
* Added a stub implementation of a search service depending on APIList.

Demo: http://nitweb.moz-code.org/

Pull-Request: #2157
Reviewed-by: Jean Privat <jean@pryen.org>

15 files changed:
contrib/benitlux/src/client/base.nit
contrib/benitlux/src/client/features/translations.nit
contrib/benitlux/src/client/views/user_views.nit
contrib/benitlux/src/server/benitlux_social.nit
contrib/nitiwiki/src/wiki_base.nit
contrib/nitiwiki/src/wiki_html.nit
contrib/nitiwiki/src/wiki_links.nit
contrib/refund/src/refund_json.nit
contrib/simplan/simplan.nit
lib/app/README.md
lib/app/http_request.nit
lib/app/package.ini
lib/core/text/abstract_text.nit
share/man/nitc.md
src/rapid_type_analysis.nit

index bcc1efd..438a125 100644 (file)
@@ -117,14 +117,17 @@ class BenitluxHttpRequest
                        app.user = null
                        return true
                else if res isa BenitluxError then
-                       app.feedback((res.user_message or else res.message).t)
+                       feedback((res.user_message or else res.message).t)
                        return true
                else if res isa Error then
-                       app.feedback res.message.t
+                       feedback res.message.t
                        return true
                end
                return false
        end
+
+       # Show feedback pertinent to the user, defaults to a platform specific popup
+       fun feedback(text: String) do app.feedback text
 end
 
 # Async request with services to act on the windows of the app
index 02deb61..be42f57 100644 (file)
@@ -63,13 +63,23 @@ do
        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["Repeat password"] = "Répéter le mot de passe"
        map["Email"] = "Courriel"
        map["Login"] = "Se connecter"
+       map["Loging in..."] = "Authentification..."
        map["Logout"] = "Se déconnecter"
        map["Signup"] = "Créer un compte"
+       map["Signing up..."] = "Création du compte..."
+
+       map["Passwords must be composed of at least 6 characters."] = "Le mot de passe doit avoir au moins 6 charactères."
+       map["Fill the following fields to sign up."] = "Remplissez les champs suivants pour créer un compte."
+
+       map["Passwords do not match."] = "Les mots de passe ne correspondent pas."
+       map["Invalid username."] = "Nom d'utilisateur invalide."
+       map["Invalid password."] = "Mot de passe invalide."
+       map["Username already in use."] = "Le nom d'utilisateur est déjà réservé."
+       map["Invalid username and password combination."] = "La combinaison de nom et mot de passe n'est pas reconnue."
 
        # Social views
        map["Follow"] = "Suivre"
index e45fcb5..b69b1db 100644 (file)
@@ -86,38 +86,50 @@ end
 class SignupWindow
        super Window
 
-       # Main window layout
-       var layout = new ListLayout(parent=self)
+       private var list = new ListLayout(parent=self)
+       private var lbl_feedback = new Label(parent=list, text="Welcome")
 
-       private var lbl_welcome = new Label(parent=layout, text="Welcome")
+       private var layout_login = new VerticalLayout(parent=list)
+
+       # ---
+       # First the login options
 
        # Name
-       private var name_line = new HorizontalLayout(parent=layout)
+       private var name_line = new HorizontalLayout(parent=layout_login)
        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)
+       # Password
+       private var pass_line = new HorizontalLayout(parent=layout_login)
        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,
+       private var lbl_pass_desc = new Label(parent=layout_login, size = 0.5,
                text="Passwords must be composed of at least 6 characters.".t)
 
-       private var but_login = new Button(parent=layout, text="Login".t)
+       private var but_login = new Button(parent=layout_login, text="Login".t)
+
+       # ---
+       # Then, the signup options
+
+       private var layout_register = new VerticalLayout(parent=list)
+
+       private var lbl_signup_desc = new Label(parent=layout_register, size = 0.5,
+               text="Fill the following fields to sign up.".t)
+
+       # Repeat password
+       private var pass_line2 = new HorizontalLayout(parent=layout_register)
+       private var lbl_pass2 = new Label(parent=pass_line2, text="Repeat password".t)
+       private var txt_pass2 = new TextInput(parent=pass_line2, is_password=true)
 
        # Email
-       private var email_line = new HorizontalLayout(parent=layout)
+       private var email_line = new HorizontalLayout(parent=layout_register)
        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="")
+       private var but_signup = new Button(parent=layout_register, text="Signup".t)
 
        init
        do
-               lbl_pass_desc.size = 0.5
-
                for c in [but_login, but_signup] do
                        c.observers.add self
                end
@@ -133,25 +145,32 @@ class SignupWindow
 
                                var name = txt_name.text
                                if name == null or not name.name_is_ok then
-                                       feedback "Invalid name".t
+                                       feedback "Invalid username.".t
                                        return
                                end
 
                                var pass = txt_pass.text
                                if pass == null or not pass.pass_is_ok then
-                                       feedback "Invalid password".t
+                                       feedback "Invalid password.".t
                                        return
                                end
 
                                if sender == but_login then
+                                       feedback "Logging in...".t
                                        (new LoginOrSignupAction(self, "rest/login?name={name}&pass={pass.pass_hash}")).start
                                else if sender == but_signup then
+                                       if pass != txt_pass2.text then
+                                               feedback "Passwords do not match.".t
+                                               return
+                                       end
+
                                        var email = txt_email.text
                                        if email == null or email.is_empty then
                                                feedback "Invalid email".t
                                                return
                                        end
 
+                                       feedback "Signing up...".t
                                        (new LoginOrSignupAction(self, "rest/signup?name={name}&pass={pass.pass_hash}&email={email}")).start
                                end
                        end
@@ -189,27 +208,6 @@ class LoginOrSignupAction
 
                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
+       redef fun feedback(text) do window.feedback text
 end
index bdd83b4..7764c75 100644 (file)
@@ -182,7 +182,7 @@ GROUP BY beer0, beer1""") else
                # Check if already in user
                var stmt = select("ROWID FROM users WHERE lower({user.to_sql_string}) = lower(name)")
                assert stmt != null else print_error "Select 'sign_up' failed with: {error or else "?"}"
-               if not stmt.iterator.to_a.is_empty then return "Username already in use"
+               if not stmt.iterator.to_a.is_empty then return "Username already in use."
 
                # Check email use
                stmt = select("ROWID FROM users WHERE lower({email.to_sql_string}) = lower(email)")
index 70cf35f..d3adc26 100644 (file)
@@ -94,7 +94,7 @@ class Nitiwiki
                                var entry = entries[path]
                                if not entry.is_dirty then continue
                                var name = entry.name
-                               if entry.has_source then name = entry.src_path.to_s
+                               if entry.has_source then name = entry.src_path.as(not null)
                                if entry.is_new then
                                        print " + {name}"
                                else
@@ -141,7 +141,7 @@ class Nitiwiki
        fun need_render(src, target: String): Bool do
                if force_render then return true
                if not target.file_exists then return true
-               return src.file_stat.mtime >= target.file_stat.mtime
+               return src.file_stat.as(not null).mtime >= target.file_stat.as(not null).mtime
        end
 
        # Create a new `WikiSection`.
@@ -244,7 +244,7 @@ class Nitiwiki
        # Used to translate ids in beautiful page names.
        fun pretty_name(name: String): String do
                name = name.replace("_", " ")
-               name = name.capitalized
+               name = name.capitalized(keep_upper=true)
                return name
        end
 end
@@ -283,7 +283,7 @@ abstract class WikiEntry
        # Returns `-1` if not `has_source`.
        fun create_time: Int do
                if not has_source then return -1
-               return src_full_path.file_stat.ctime
+               return src_full_path.as(not null).file_stat.as(not null).ctime
        end
 
        # Entry last modification time.
@@ -291,7 +291,7 @@ abstract class WikiEntry
        # Returns `-1` if not `has_source`.
        fun last_edit_time: Int do
                if not has_source then return -1
-               return src_full_path.file_stat.mtime
+               return src_full_path.as(not null).file_stat.as(not null).mtime
        end
 
        # Entry list rendering time.
@@ -299,7 +299,7 @@ abstract class WikiEntry
        # Returns `-1` if `is_new`.
        fun last_render_time: Int do
                if is_new then return -1
-               return out_full_path.file_stat.mtime
+               return out_full_path.file_stat.as(not null).mtime
        end
 
        # Entries hierarchy
@@ -400,7 +400,7 @@ abstract class WikiEntry
        # then returns the main wiki template file.
        fun template_file: String do
                if is_root then return wiki.config.template_file
-               return parent.template_file
+               return parent.as(not null).template_file
        end
 
        # Header template file for `self`.
@@ -408,7 +408,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun header_file: String do
                if is_root then return wiki.config.header_file
-               return parent.header_file
+               return parent.as(not null).header_file
        end
 
        # Footer template file for `self`.
@@ -416,7 +416,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun footer_file: String do
                if is_root then return wiki.config.footer_file
-               return parent.footer_file
+               return parent.as(not null).footer_file
        end
 
        # Menu template file for `self`.
@@ -424,7 +424,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun menu_file: String do
                if is_root then return wiki.config.menu_file
-               return parent.menu_file
+               return parent.as(not null).menu_file
        end
 
        # Display the entry `name`.
@@ -442,7 +442,7 @@ class WikiSection
 
        redef fun title do
                if has_config then
-                       var title = config.title
+                       var title = config.as(not null).title
                        if title != null then return title
                end
                return super
@@ -452,7 +452,7 @@ class WikiSection
        #
        # Hidden section are rendered but not linked in menus.
        fun is_hidden: Bool do
-               if has_config then return config.is_hidden
+               if has_config then return config.as(not null).is_hidden
                return false
        end
 
@@ -461,7 +461,7 @@ class WikiSection
                if parent == null then
                        return wiki.config.source_dir
                else
-                       return wiki.expand_path(parent.src_path, name)
+                       return wiki.expand_path(parent.as(not null).src_path, name)
                end
        end
 
@@ -486,41 +486,41 @@ class WikiSection
        # Also check custom config.
        redef fun template_file do
                if has_config then
-                       var tpl = config.template_file
+                       var tpl = config.as(not null).template_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.template_file
-               return parent.template_file
+               return parent.as(not null).template_file
        end
 
        # Also check custom config.
        redef fun header_file do
                if has_config then
-                       var tpl = config.header_file
+                       var tpl = config.as(not null).header_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.header_file
-               return parent.header_file
+               return parent.as(not null).header_file
        end
 
        # Also check custom config.
        redef fun footer_file do
                if has_config then
-                       var tpl = config.footer_file
+                       var tpl = config.as(not null).footer_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.footer_file
-               return parent.footer_file
+               return parent.as(not null).footer_file
        end
 
        # Also check custom config.
        redef fun menu_file do
                if has_config then
-                       var tpl = config.menu_file
+                       var tpl = config.as(not null).menu_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.menu_file
-               return parent.menu_file
+               return parent.as(not null).menu_file
        end
 end
 
@@ -534,7 +534,8 @@ class WikiArticle
        # Articles can only have `WikiSection` as parents.
        redef type PARENT: WikiSection
 
-       redef fun title: String do
+       redef fun title do
+               var parent = self.parent
                if name == "index" and parent != null then return parent.title
                return super
        end
@@ -551,7 +552,7 @@ class WikiArticle
                content = md
        end
 
-       redef var src_full_path: nullable String = null
+       redef var src_full_path = null
 
        redef fun src_path do
                var src_full_path = self.src_full_path
@@ -567,7 +568,7 @@ class WikiArticle
        # REQUIRE: `has_source`.
        var md: nullable String is lazy do
                if not has_source then return null
-               var file = new FileReader.open(src_full_path.to_s)
+               var file = new FileReader.open(src_full_path.as(not null))
                var md = file.read_all
                file.close
                return md
@@ -578,7 +579,7 @@ class WikiArticle
        redef fun is_dirty do
                if super then return true
                if has_source then
-                       return wiki.need_render(src_full_path.to_s, out_full_path)
+                       return wiki.need_render(src_full_path.as(not null), out_full_path)
                end
                return false
        end
@@ -779,7 +780,7 @@ class WikiConfig
        var sidebar_blocks: Array[String] is lazy do
                var res = new Array[String]
                if not has_key("wiki.sidebar.blocks") then return res
-               for val in at("wiki.sidebar.blocks").values do
+               for val in at("wiki.sidebar.blocks").as(not null).values do
                        res.add val
                end
                return res
index ab79f09..15ae4d7 100644 (file)
@@ -70,7 +70,8 @@ end
 redef class WikiSection
 
        # Output directory (where to ouput the HTML pages for this section).
-       redef fun out_path: String do
+       redef fun out_path do
+               var parent = self.parent
                if parent == null then
                        return wiki.config.out_dir
                else
@@ -104,7 +105,7 @@ redef class WikiSection
        # Copy attached files from `src_path` to `out_path`.
        private fun copy_files do
                assert has_source
-               var dir = src_full_path.to_s
+               var dir = src_full_path.as(not null).to_s
                for name in dir.files do
                        if name == wiki.config_filename then continue
                        if name.has_suffix(".md") then continue
@@ -167,7 +168,8 @@ end
 
 redef class WikiArticle
 
-       redef fun out_path: String do
+       redef fun out_path do
+               var parent = self.parent
                if parent == null then
                        return wiki.expand_path(wiki.config.out_dir, "{name}.html")
                else
index ac3e171..33dc61f 100644 (file)
@@ -31,7 +31,7 @@ redef class Nitiwiki
        # Returns `null` if no article can be found.
        fun lookup_entry_by_name(context: WikiEntry, name: String): nullable WikiEntry do
                var section: nullable WikiEntry = context.parent or else context
-               var res = section.lookup_entry_by_name(name)
+               var res = section.as(not null).lookup_entry_by_name(name)
                if res != null then return res
                while section != null do
                        if section.name == name then return section
@@ -52,7 +52,7 @@ redef class Nitiwiki
        # Returns `null` if no article can be found.
        fun lookup_entry_by_title(context: WikiEntry, title: String): nullable WikiEntry do
                var section: nullable WikiEntry = context.parent or else context
-               var res = section.lookup_entry_by_title(title)
+               var res = section.as(not null).lookup_entry_by_title(title)
                if res != null then return res
                while section != null do
                        if section.title.to_lower == title.to_lower then return section
@@ -200,6 +200,7 @@ redef class WikiArticle
        fun is_index: Bool do return name == "index"
 
        redef fun href do
+               var parent = self.parent
                if parent == null then
                        return "{name}.html"
                else
index 33032b3..4da71f5 100644 (file)
@@ -72,6 +72,9 @@ redef class RefundProcessor
                if json isa JsonParseError then
                        die("Wrong input file ({json.message})")
                        abort
+               else if json == null then
+                       die("Unable to parse input file as json (got null)")
+                       abort
                else if not json isa JsonObject then
                        die("Wrong input type (expected JsonObject got {json.class_name})")
                        abort
@@ -130,7 +133,7 @@ redef class RefundProcessor
                return new RefundStats.from_json(content)
        end
 
-       redef fun save_stats(stats: RefundStats) do
+       redef fun save_stats(stats) do
                write_output(stats.to_json.to_pretty_json, stats_file)
        end
 end
@@ -165,13 +168,19 @@ redef class ReclamationSheet
                proc.check_key(json, "reclamations")
                var res = new Array[Reclamation]
                var recls = json["reclamations"]
-               if not recls isa JsonArray then
+               if recls == null then
+                       proc.die("Wrong type for `number` (expected JsonArray got null)")
+                       abort
+               else if not recls isa JsonArray then
                        proc.die("Wrong type for `number` (expected JsonArray got {recls.class_name})")
                        abort
                end
                var i = 0
                for obj in recls do
-                       if not obj isa JsonObject then
+                       if obj == null then
+                               proc.die("Wrong type for `reclamations#{i}` (expected JsonObject got null)")
+                               abort
+                       else if not obj isa JsonObject then
                                proc.die("Wrong type for `reclamations#{i}` " +
                                        "(expected JsonObject got {obj.class_name})")
                                abort
@@ -197,7 +206,10 @@ redef class ReclFile
        init from_json(proc: RefundProcessor, json: JsonObject) do
                proc.check_key(json, "dossier")
                var id = json["dossier"]
-               if not id isa String then
+               if id == null then
+                       proc.die("Wrong type for `dossier` (expected String got null)")
+                       abort
+               else if not id isa String then
                        proc.die("Wrong type for `dossier` (expected String got {id.class_name})")
                        abort
                end
@@ -232,7 +244,10 @@ redef class ReclMonth
        init from_json(proc: RefundProcessor, json: JsonObject) do
                proc.check_key(json, "mois")
                var month = json["mois"]
-               if not month isa String then
+               if month == null then
+                       proc.die("Wrong type for `mois` (expected String got null)")
+                       return
+               else if not month isa String then
                        proc.die("Wrong type for `mois` (expected String got {month.class_name})")
                        return
                end
@@ -264,7 +279,10 @@ redef class ReclDate
        init from_json(proc: RefundProcessor, json: JsonObject) do
                proc.check_key(json, "date")
                var date = json["date"]
-               if not date isa String then
+               if date == null then
+                       proc.die("Wrong type for `date` (expected String got null)")
+                       abort
+               else if not date isa String then
                        proc.die("Wrong type for `date` (expected String got {date.class_name})")
                        abort
                end
@@ -302,7 +320,10 @@ redef class Reclamation
        private fun parse_care_id(proc: RefundProcessor, json: JsonObject): Int do
                proc.check_key(json, "soin")
                var id = json["soin"]
-               if not id isa Int then
+               if id == null then
+                       proc.die("Wrong type for `soin` (expected Int got null)")
+                       abort
+               else if not id isa Int then
                        proc.die("Wrong type for `soin` (expected Int got {id.class_name})")
                        abort
                end
@@ -313,7 +334,10 @@ redef class Reclamation
        private fun parse_fees(proc: RefundProcessor, json: JsonObject): Dollar do
                proc.check_key(json, "montant")
                var fees = json["montant"]
-               if not fees isa String then
+               if fees == null then
+                       proc.die("Wrong type for `fees` (expected String got null)")
+                       abort
+               else if not fees isa String then
                        proc.die("Wrong type for `fees` (expected String got {fees.class_name})")
                        abort
                end
index ba32497..301c564 100644 (file)
@@ -80,7 +80,7 @@ class PlanProblem
                        print n.message
                        exit 1
                end
-               n = n.children.first.children.first.as(not null)
+               n = n.children.first.as(not null).children.first.as(not null)
                if n isa Nplan then
                        print "Error: expected a problem, got a plan."
                        exit 1
@@ -88,7 +88,7 @@ class PlanProblem
                assert n isa Nproblem
 
                # Load all locations
-               for n2 in n.n_locations.n_list.children do
+               for n2 in n.n_locations.n_list.as(not null).children do
                        var e = new Location(locations.length, n2.n_name.text, n2.n_x.text.to_f, n2.n_y.text.to_f)
                        assert not locations.has_key(e.name)
                        locations[e.name] = e
@@ -97,7 +97,7 @@ class PlanProblem
 
                # Load all roads
                var nbr = 0
-               for n2 in n.n_roads.n_list.children do
+               for n2 in n.n_roads.n_list.as(not null).children do
                        var o = locations.get_or_null(n2.n_orig.text)
                        var d = locations.get_or_null(n2.n_dest.text)
                        assert o != null and d != null
@@ -132,7 +132,7 @@ class PlanProblem
 
                # Load the robot
                var robot = null
-               for n2 in n.n_robots.n_list.children do
+               for n2 in n.n_robots.n_list.as(not null).children do
                        var name = n2.n_name.text
                        robot = locations.get_or_null(n2.n_emplacement.text)
                        assert name == robot_name and robot != null
@@ -142,7 +142,7 @@ class PlanProblem
 
                # Load the parcels
                var parcel_locations = new Array[nullable Location]
-               for n2 in n.n_parcels.n_list.children do
+               for n2 in n.n_parcels.n_list.as(not null).children do
                        var name = n2.n_name.text
                        var e = locations.get_or_null(n2.n_emplacement.text)
                        assert e != null
@@ -155,7 +155,7 @@ class PlanProblem
                print "# {parcels.length} parcels"
 
                # Load the goal of parcels
-               for n2 in n.n_goal.n_list.children do
+               for n2 in n.n_goal.n_list.as(not null).children do
                        var parcel = parcel_by_name.get_or_null(n2.n_name.text)
                        var e = locations.get_or_null(n2.n_emplacement.text)
                        assert parcel != null and e != null
@@ -179,7 +179,7 @@ class PlanProblem
                        print n.message
                        exit 1
                end
-               n = n.children.first.children.first.as(not null)
+               n = n.children.first.as(not null).children.first.as(not null)
                if n isa Nproblem then
                        print "Error: expected a plan, got a problem."
                        exit 1
@@ -189,7 +189,7 @@ class PlanProblem
                var res = new Plan(self)
                var e = initial_state
                var cost = 0.0
-               for n2 in n.n_actions.children do
+               for n2 in n.n_actions.as(not null).children do
                        if n2 isa Naction_load then
                                var parcel = parcel_by_name.get_or_null(n2.n_parcel.text)
                                assert parcel != null
index c5caa4b..0d2d24c 100644 (file)
@@ -5,13 +5,14 @@ The framework provides services to manage common needs of modern mobile applicat
 * Life-cycle
 * User interface
 * Persistence
+* Async HTTP requests
 * Package metadata
 * Compilation and packaging
 
 The features offered by _app.nit_ are common to all platforms, but
 may not be available on all devices.
 
-## Application Life-Cycle
+# Application Life-Cycle
 
 The _app.nit_ application life-cycle is compatible with all target platforms.
 It relies on the following sequence of events, represented here by their callback method name:
@@ -44,7 +45,7 @@ The `App` instance is the first to be notified of these events.
 Other UI elements, from the `ui` submodule, are notified of the same events using a simple depth first visit.
 So all UI elements can react separately to live-cycle events.
 
-## User Interface
+# User Interface
 
 The `app::ui` module defines an abstract API to build a portable graphical application.
 The API is composed of interactive `Control`s, visible `View`s and an active `Window`.
@@ -65,21 +66,20 @@ So there is two ways  to customize the behavior on a given event:
 
 * Add an observer to a `Button` instance, and implement `on_event` in the observer.
 
-### Usage Example
+## Usage Example
 
 The calculator example (at `../../examples/calculator/src/calculator.nit`) is a concrete,
 simple and complete use of the _app.nit_ portable UI.
 
-### Platform-specific UI
+## Platform-specific UI
 
 You can go beyond the portable UI API of _app.nit_ by using the natives services of a platform.
 
 The suggested approach is to use platform specific modules to customize the application on a precise platform.
-This module redefine `Window::on_start` to call the native language of the platform and setup a native UI.
+See the calculator example for an adaptation of the UI on Android,
+the interesting module is in this repository at ../../examples/calculator/src/android_calculator.nit
 
-_TODO complete description and add concrete examples_
-
-## Persistent State with data\_store
+# Persistent State with data\_store
 
 _app.nit_ offers the submodule `app::data_store` to easily save the application state and user preferences.
 The service is accessible by the method `App::data_store`. The `DataStore` itself defines 2 methods:
@@ -90,7 +90,7 @@ Pass `null` to clear the value associated to a key.
 * `DataStore::[]` returns the object associated to a `String` key.
 It returns `null` if nothing is associated to the key.
 
-### Usage Example
+## Usage Example
 
 ~~~
 import app::data_store
@@ -123,7 +123,14 @@ redef class App
 end
 ~~~
 
-## Metadata annotations
+# Async HTTP request
+
+The module `app::http_request` provides services to execute asynchronous HTTP request.
+The class `AsyncHttpRequest` hides the complex parallel logic and
+lets the user implement methods acting only on the UI thread.
+See the documentation of `AsyncHttpRequest` for more information.
+
+# Metadata annotations
 
 The _app.nit_ framework defines three annotations to customize the application package.
 
@@ -142,7 +149,7 @@ The _app.nit_ framework defines three annotations to customize the application p
   The special function `git_revision` will use the prefix of the hash of the latest git commit.
   By default, the version is 0.1.
 
-### Usage Example
+## Usage Example
 
 ~~~
 module my_module is
@@ -152,33 +159,38 @@ module my_module is
 end
 ~~~
 
-## Compiling and Packaging an Application
+# Compiling and Packaging an Application
 
 The Nit compiler detects the target platform from the importations and generates the appropriate application format and package.
 
 Applications using only the portable services of _app.nit_ require some special care at compilation.
 Such an application, let's say `calculator.nit`, does not depend on a specific platform and use the portable UI.
-The target platform must be specifed to the compiler for it to produce the correct application package.
+The target platform must be specified to the compiler for it to produce the correct application package.
 There is two main ways to achieve this goal:
 
-* The the mixin option (`-m path`) loads an additionnal module before compiling.
+* The mixin option (`-m module`) imports an additional module before compiling.
   It can be used to load platform specific implementations of the _app.nit_ portable UI.
 
   ~~~
   # GNU/Linux version, using GTK
-  nitc calculator.nit -m NIT_DIR/lib/linux/ui.nit
+  nitc calculator.nit -m linux
 
   # Android version
-  nitc calculator.nit -m NIT_DIR/lib/android/ui/
+  nitc calculator.nit -m android
+
+  # iOS version
+  nitc calculator.nit -m ios
   ~~~
 
 * A common alternative for larger projects is to use platform specific modules.
-  Continuing with the `calculator.nit` example, it can be accompagnied by the module `calculator_linux.nit`.
-  This module imports both `calculator` and `linux::ui`, and can also use other GNU/Linux specific code.
+  Continuing with the calculator example, it is adapted for Android by the module `android_calculator.nit`.
+  This module imports both `calculator` and `android`, it can then use Android specific code.
 
   ~~~
-  module calculator_linux
+  module android_calculator
 
   import calculator
-  import linux::ui
+  import android
+
+  # ...
   ~~~
index 9a5feca..694ba33 100644 (file)
@@ -28,14 +28,21 @@ redef class App
        fun run_on_ui_thread(task: Task) is abstract
 end
 
-# Thread executing an HTTP request and deserializing JSON asynchronously
+# Thread executing an HTTP request asynchronously
 #
-# This class defines four methods acting on the main/UI thread,
-# they should be implemented as needed:
-# * before
-# * on_load
-# * on_fail
-# * after
+# The request is sent to `rest_server_uri / rest_action`.
+#
+# If `deserialize_json`, the default behavior, the response is deserialized from JSON
+#
+# If `delay > 0.0`, sending the reqest is delayed by the given `delay` in seconds.
+# It can be used to delay resending a request on error.
+#
+# Four callback methods act on the main/UI thread,
+# they should be implemented as needed in subclasses:
+# * `before`
+# * `on_load`
+# * `on_fail`
+# * `after`
 class AsyncHttpRequest
        super Thread
 
index d5c48c1..88a75fb 100644 (file)
@@ -1,6 +1,6 @@
 [package]
 name=app
-tags=lib
+tags=lib,mobile
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
index ed04967..8fefb24 100644 (file)
@@ -1360,30 +1360,19 @@ abstract class String
        # Letters that follow a letter are lowercased
        # Letters that follow a non-letter are upcased.
        #
+       # If `keep_upper = true`, already uppercase letters are not lowercased.
+       #
        # SEE : `Char::is_letter` for the definition of letter.
        #
        #     assert "jAVASCRIPT".capitalized == "Javascript"
        #     assert "i am root".capitalized == "I Am Root"
        #     assert "ab_c -ab0c ab\nc".capitalized == "Ab_C -Ab0C Ab\nC"
-       fun capitalized: SELFTYPE do
+       #     assert "preserve my ACRONYMS".capitalized(keep_upper=true) == "Preserve My ACRONYMS"
+       fun capitalized(keep_upper: nullable Bool): SELFTYPE do
                if length == 0 then return self
 
                var buf = new Buffer.with_cap(length)
-
-               var curr = chars[0].to_upper
-               var prev = curr
-               buf[0] = curr
-
-               for i in [1 .. length[ do
-                       prev = curr
-                       curr = self[i]
-                       if prev.is_letter then
-                               buf[i] = curr.to_lower
-                       else
-                               buf[i] = curr.to_upper
-                       end
-               end
-
+               buf.capitalize(keep_upper=keep_upper, src=self)
                return buf.to_s
        end
 end
@@ -1478,6 +1467,13 @@ abstract class Buffer
        # Letters that follow a letter are lowercased
        # Letters that follow a non-letter are upcased.
        #
+       # If `keep_upper = true`, uppercase letters are not lowercased.
+       #
+       # When `src` is specified, this method reads from `src` instead of `self`
+       # but it still writes the result to the beginning of `self`.
+       # This requires `self` to have the capacity to receive all of the
+       # capitalized content of `src`.
+       #
        # SEE: `Char::is_letter` for the definition of a letter.
        #
        #     var b = new FlatBuffer.from("jAVAsCriPt")
@@ -1489,16 +1485,32 @@ abstract class Buffer
        #     b = new FlatBuffer.from("ab_c -ab0c ab\nc")
        #     b.capitalize
        #     assert b == "Ab_C -Ab0C Ab\nC"
-       fun capitalize do
+       #
+       #     b = new FlatBuffer.from("12345")
+       #     b.capitalize(src="foo")
+       #     assert b == "Foo45"
+       #
+       #     b = new FlatBuffer.from("preserve my ACRONYMS")
+       #     b.capitalize(keep_upper=true)
+       #     assert b == "Preserve My ACRONYMS"
+       fun capitalize(keep_upper: nullable Bool, src: nullable Text) do
+               src = src or else self
+               var length = src.length
                if length == 0 then return
-               var c = self[0].to_upper
+               keep_upper = keep_upper or else false
+
+               var c = src[0].to_upper
                self[0] = c
                var prev = c
                for i in [1 .. length[ do
                        prev = c
-                       c = self[i]
+                       c = src[i]
                        if prev.is_letter then
-                               self[i] = c.to_lower
+                               if keep_upper then
+                                       self[i] = c
+                               else
+                                       self[i] = c.to_lower
+                               end
                        else
                                self[i] = c.to_upper
                        end
index 2927a24..a316782 100644 (file)
@@ -386,7 +386,7 @@ Put primitive attributes in a box instead of an union.
 ### `--no-shortcut-equal`
 Always call == in a polymorphic way.
 
-### `--no-tag-primitive`
+### `--no-tag-primitives`
 Use only boxes for primitive types.
 
 The separate compiler uses tagged values to encode common primitive types like Int, Bool and Char.
@@ -410,9 +410,6 @@ Use an indirection when calling.
 
 Just add the trampolines of `--substitute-monomorph` without doing any additionnal optimizations.
 
-### `--no-tag-primitives`
-Use only boxes for primitive types.
-
 ## INTERNAL OPTIONS
 
 These options can be used to control the fine behavior of the tool.
index 2d621e8..88e2b7e 100644 (file)
@@ -255,6 +255,8 @@ class RapidTypeAnalysis
                                add_cast(paramtype)
                        end
 
+                       if mmethoddef.is_abstract then continue
+
                        var npropdef = modelbuilder.mpropdef2node(mmethoddef)
 
                        if npropdef isa AClassdef then