Merge: Docunit --no-color
authorJean Privat <jean@pryen.org>
Fri, 27 May 2016 18:47:56 +0000 (14:47 -0400)
committerJean Privat <jean@pryen.org>
Fri, 27 May 2016 18:47:56 +0000 (14:47 -0400)
Small last nitpicks on nitnuit

* --no-color mode
* better summary at the end
* working dir no more hidden and removed on success

Pull-Request: #2131

16 files changed:
lib/core/file.nit
lib/nitcorn/file_server.nit
lib/popcorn/README.md
lib/popcorn/examples/angular/example_angular.nit
lib/popcorn/examples/static_files/example_static_default.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/public/default.html [new file with mode: 0644]
lib/popcorn/pop_handlers.nit
lib/popcorn/tests/res/test_example_angular.res
lib/popcorn/tests/res/test_example_static_default.res [new file with mode: 0644]
lib/popcorn/tests/test_example_angular.nit
lib/popcorn/tests/test_example_static_default.nit [new file with mode: 0644]
lib/readline.ini [new file with mode: 0644]
lib/readline.nit [new file with mode: 0644]
tests/sav/test_readline.res [new file with mode: 0644]
tests/test_readline.inputs [new file with mode: 0644]
tests/test_readline.nit [new file with mode: 0644]

index def1cd2..7f1d481 100644 (file)
@@ -663,6 +663,19 @@ class Path
                return res
        end
 
+       # Is `self` the path to an existing directory ?
+       #
+       # ~~~nit
+       # assert ".".to_path.is_dir
+       # assert not "/etc/issue".to_path.is_dir
+       # assert not "/should/not/exist".to_path.is_dir
+       # ~~~
+       fun is_dir: Bool do
+               var st = stat
+               if st == null then return false
+               return st.is_dir
+       end
+
        # Delete a directory and all of its content
        #
        # Does not go through symbolic links and may get stuck in a cycle if there
index 5995558..a9ef5f6 100644 (file)
@@ -74,6 +74,12 @@ class FileServer
        # Show directory listing?
        var show_directory_listing = true is writable
 
+       # Default file returned when no static file matches the requested URI.
+       #
+       # If no `default_file` is provided, the FileServer responds 404 error to
+       # unmatched queries.
+       var default_file: nullable String = null is writable
+
        redef fun answer(request, turi)
        do
                var response
@@ -86,15 +92,14 @@ class FileServer
                # This make sure that the requested file is within the root folder.
                if (local_file + "/").has_prefix(root) then
                        # Does it exists?
-                       if local_file.file_exists then
-                               if local_file.file_stat.is_dir then
+                       var file_stat = local_file.file_stat
+                       if file_stat != null then
+                               if file_stat.is_dir then
                                        # If we target a directory without an ending `/`,
                                        # redirect to the directory ending with `/`.
-                                       if not request.uri.is_empty and
-                                          request.uri.chars.last != '/' then
-                                               response = new HttpResponse(303)
-                                               response.header["Location"] = request.uri + "/"
-                                               return response
+                                       var uri = request.uri
+                                       if not uri.is_empty and uri.chars.last != '/' then
+                                               return answer_redirection(request.uri + "/")
                                        end
 
                                        # Show index file instead of the directory listing
@@ -108,34 +113,94 @@ class FileServer
                                        end
                                end
 
-                               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
+                               file_stat = local_file.file_stat
+                               if show_directory_listing and file_stat != null and file_stat.is_dir then
+                                       response = answer_directory_listing(request, turi, local_file)
+                               else if file_stat != null and not file_stat.is_dir then # It's a single file
+                                       response = answer_file(local_file)
+                               else response = answer_default
+                       else response = answer_default
+               else response = new HttpResponse(403)
 
-                                       alpha_comparator.sort files
+               if response.status_code != 200 then
+                       var tmpl = error_page(response.status_code)
+                       if header != null and tmpl isa ErrorTemplate then tmpl.header = header
+                       response.body = tmpl.to_s
+               end
 
-                                       var links = new Array[String]
-                                       if turi.length > 1 then
-                                               var path = (request.uri + "/..").simplify_path
-                                               links.add "<a href=\"{path}/\">..</a>"
-                                       end
-                                       for file in files do
-                                               var local_path = local_file.join_path(file).simplify_path
-                                               var web_path = file.simplify_path
-                                               if local_path.file_stat.is_dir then web_path = web_path + "/"
-                                               links.add "<a href=\"{web_path}\">{file}</a>"
-                                       end
+               return response
+       end
+
+       # Answer the `default_file` if any.
+       fun answer_default: HttpResponse do
+               var default_file = self.default_file
+               if default_file == null then
+                       return new HttpResponse(404)
+               end
+
+               var local_file = (root / default_file).simplify_path
+               return answer_file(local_file)
+       end
 
-                                       var header = self.header
-                                       var header_code
-                                       if header != null then
-                                               header_code = header.write_to_string
-                                       else header_code = ""
+       # Answer a 303 redirection to `location`.
+       fun answer_redirection(location: String): HttpResponse do
+               var response = new HttpResponse(303)
+               response.header["Location"] = location
+               return response
+       end
+
+       # Build a reponse containing a single `local_file`.
+       #
+       # Returns a 404 error if local_file does not exists.
+       fun answer_file(local_file: String): HttpResponse do
+               if not local_file.file_exists then return new HttpResponse(404)
+
+               var response = new HttpResponse(200)
+               response.files.add local_file
+
+               # Set Content-Type depending on the file extension
+               var ext = local_file.file_extension
+               if ext != null then
+                       var media_type = media_types[ext]
+                       if media_type != null then
+                               response.header["Content-Type"] = media_type
+                       else response.header["Content-Type"] = "application/octet-stream"
+               end
 
-                                       response = new HttpResponse(200)
-                                       response.body = """
+               # Cache control
+               response.header["cache-control"] = cache_control
+               return response
+       end
+
+       # Answer with a directory listing for files within `local_files`.
+       fun answer_directory_listing(request: HttpRequest, turi, local_file: String): HttpResponse do
+               # Show the directory listing
+               var title = turi
+               var files = local_file.files
+
+               alpha_comparator.sort files
+
+               var links = new Array[String]
+               if turi.length > 1 then
+                       var path = (request.uri + "/..").simplify_path
+                       links.add "<a href=\"{path}/\">..</a>"
+               end
+               for file in files do
+                       var local_path = local_file.join_path(file).simplify_path
+                       var web_path = file.simplify_path
+                       var file_stat = local_path.file_stat
+                       if file_stat != null and file_stat.is_dir then web_path = web_path + "/"
+                       links.add "<a href=\"{web_path}\">{file}</a>"
+               end
+
+               var header = self.header
+               var header_code
+               if header != null then
+                       header_code = header.write_to_string
+               else header_code = ""
+
+               var response = new HttpResponse(200)
+               response.body = """
 <!DOCTYPE html>
 <head>
        <meta charset="utf-8">
@@ -157,33 +222,7 @@ class FileServer
 </body>
 </html>"""
 
-                                       response.header["Content-Type"] = media_types["html"].as(not null)
-                               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
-                                       if ext != null then
-                                               var media_type = media_types[ext]
-                                               if media_type != null then
-                                                       response.header["Content-Type"] = media_type
-                                               else response.header["Content-Type"] = "application/octet-stream"
-                                       end
-
-                                       # Cache control
-                                       response.header["cache-control"] = cache_control
-
-                               else response = new HttpResponse(404)
-                       else response = new HttpResponse(404)
-               else response = new HttpResponse(403)
-
-               if response.status_code != 200 then
-                       var tmpl = error_page(response.status_code)
-                       if header != null and tmpl isa ErrorTemplate then tmpl.header = header
-                       response.body = tmpl.to_s
-               end
-
+               response.header["Content-Type"] = media_types["html"].as(not null)
                return response
        end
 end
index a136621..2bb6646 100644 (file)
@@ -175,6 +175,17 @@ directory from where you launch your app.
 If you run the app from another directory, it’s safer to use the absolute path of
 the directory that you want to serve.
 
+In some cases, you can want to redirect request to static files to a default file
+instead of returning a 404 error.
+This can be achieved by specifying a default file in the StaticHandler:
+
+~~~
+app.use("/static/", new StaticHandler("public/", "default.html"))
+~~~
+
+This way all non-matched queries to the StaticHandler will be answered with the
+`default.html` file.
+
 ## Advanced Routing
 
 **Routing** refers to the definition of application end points (URIs) and how
@@ -832,8 +843,13 @@ to your angular controller:
 import popcorn
 
 var app = new App
-app.use("/*", new StaticHandler("my-ng-app/"))
+app.use("/*", new StaticHandler("my-ng-app/", "index.html"))
 app.listen("localhost", 3000)
 ~~~
 
+Because the StaticHandler will not find the angular routes as static files,
+you must specify the path to the default angular controller.
+In this example, the StaticHandler will redirect any unknown requests to the `index.html`
+angular controller.
+
 See the examples for a more detailed use case working with a JSON API.
index ba7f4f2..9212a8e 100644 (file)
@@ -40,5 +40,5 @@ end
 
 var app = new App
 app.use("/counter", new CounterAPI)
-app.use("/*", new StaticHandler("www/"))
+app.use("/*", new StaticHandler("www/", "index.html"))
 app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/example_static_default.nit b/lib/popcorn/examples/static_files/example_static_default.nit
new file mode 100644 (file)
index 0000000..6aa5b34
--- /dev/null
@@ -0,0 +1,21 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/", "default.html"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/public/default.html b/lib/popcorn/examples/static_files/public/default.html
new file mode 100644 (file)
index 0000000..2abb789
--- /dev/null
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
index 1c20b67..5d84961 100644 (file)
@@ -235,10 +235,16 @@ class StaticHandler
        # Static files directory to serve.
        var static_dir: String
 
+       # Default file to serve if nothing matches the request.
+       #
+       # `null` for no default file.
+       var default_file: nullable String
+
        # Internal file server used to lookup and render files.
        var file_server: FileServer is lazy do
                var srv = new FileServer(static_dir)
                srv.show_directory_listing = false
+               srv.default_file = default_file
                return srv
        end
 
index c07813f..bb94961 100644 (file)
@@ -4,4 +4,19 @@
 [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
+{"label":"Visitors","value":1}
+[Client] curl -s localhost:*****/not_found
+<!DOCTYPE html>
+<html lang='en' ng-app='ng-example'>
+       <head>
+               <base href='/'>
+               <title>ng-example</title>
+       </head>
+       <body>
+               <div ng-view></div>
+
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.min.js'></script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.js'></script>
+               <script src='/javascripts/ng-example.js'></script>
+       </body>
+</html>
diff --git a/lib/popcorn/tests/res/test_example_static_default.res b/lib/popcorn/tests/res/test_example_static_default.res
new file mode 100644 (file)
index 0000000..59392f6
--- /dev/null
@@ -0,0 +1,88 @@
+
+[Client] curl -s localhost:*****/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/css/not_found.nit
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/static/css/not_found.nit
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/not_found.nit
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
index d1ab509..821aa32 100644 (file)
@@ -24,13 +24,14 @@ class TestClient
                system "curl -s {host}:{port}/counter"
                system "curl -s {host}:{port}/counter -X POST"
                system "curl -s {host}:{port}/counter"
+               system "curl -s {host}:{port}/not_found" # handled by angular controller
                return null
        end
 end
 
 var app = new App
 app.use("/counter", new CounterAPI)
-app.use("/*", new StaticHandler("www/"))
+app.use("/*", new StaticHandler("../examples/angular/www/", "index.html"))
 
 var host = test_host
 var port = test_port
diff --git a/lib/popcorn/tests/test_example_static_default.nit b/lib/popcorn/tests/test_example_static_default.nit
new file mode 100644 (file)
index 0000000..ced04e2
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base_tests
+import example_static_default
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/css/style.css"
+               system "curl -s {host}:{port}/js/app.js"
+               system "curl -s {host}:{port}/hello.html"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/css/not_found.nit"
+               system "curl -s {host}:{port}/static/css/not_found.nit"
+               system "curl -s {host}:{port}/not_found.nit"
+
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new StaticHandler("../examples/static_files/public/", "default.html"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/readline.ini b/lib/readline.ini
new file mode 100644 (file)
index 0000000..c7e2ee9
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=readline
+tags=lib
+maintainer=Frédéric Vachon <fredvac@gmail.com>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/readline.nit
+git=https://github.com/nitlang/nit.git
+git.directory=lib/readline.nit
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/lib/readline.nit b/lib/readline.nit
new file mode 100644 (file)
index 0000000..783e4e4
--- /dev/null
@@ -0,0 +1,58 @@
+# This file is part of NIT (http://www.nitlanguage.org).
+#
+# Copyright 2016 Frédéric Vachon <fredvac@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# GNU readline library wrapper
+module readline is ldflags "-lreadline"
+
+in "C" `{
+       #include <readline/readline.h>
+       #include <readline/history.h>
+`}
+
+private fun native_readline(prompt: NativeString): NativeString `{
+       return readline(prompt);
+`}
+
+private fun native_add_history(data: NativeString) `{
+       if (data == NULL) return;
+       add_history(data);
+`}
+
+# Set emacs keybindings mode
+fun set_vi_mode `{ rl_editing_mode = 0; `}
+
+# Set emacs keybindings mode
+fun set_emacs_mode `{ rl_editing_mode = 1; `}
+
+# Use the GNU Library readline function
+# Returns `null` if EOF is read
+# If `with_history` is true, it will save all commands in the history except
+# empty strings and white characters strings
+fun readline(message: String, with_history: nullable Bool): nullable String do
+       var line = native_readline(message.to_cstring)
+       if line.address_is_null then return null
+
+       var nit_str = line.to_s
+
+       if with_history != null and with_history then
+               if nit_str.trim != "" then native_add_history(line)
+       end
+
+       return nit_str
+end
+
+# Adds the data String to the history no matter what it contains
+fun add_history(data: String) do native_add_history data.to_cstring
diff --git a/tests/sav/test_readline.res b/tests/sav/test_readline.res
new file mode 100644 (file)
index 0000000..666312a
--- /dev/null
@@ -0,0 +1,9 @@
+prompt>line 1
+line 1
+prompt>line 2
+line 2
+prompt>line 2bis
+line 2bis
+prompt>line 3 \b \bine\b \b\b \b\b \b3\b \b
+line 3
+prompt>
\ No newline at end of file
diff --git a/tests/test_readline.inputs b/tests/test_readline.inputs
new file mode 100644 (file)
index 0000000..59f6fe7
--- /dev/null
@@ -0,0 +1,4 @@
+line 1
+line 2
+line 2bis
+line 3 \bine\b\b\b3\b
diff --git a/tests/test_readline.nit b/tests/test_readline.nit
new file mode 100644 (file)
index 0000000..c254b24
--- /dev/null
@@ -0,0 +1,21 @@
+# 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 readline
+
+loop
+       var line = readline("prompt>", true)
+       if line == null then break
+       print line
+end