Merge: lib/core: introduce replace_first in text/string_search
authorJean Privat <jean@pryen.org>
Tue, 6 Jun 2017 16:01:39 +0000 (12:01 -0400)
committerJean Privat <jean@pryen.org>
Tue, 6 Jun 2017 16:01:39 +0000 (12:01 -0400)
Replace the first occurrence of `pattern` with `string`

~~~nit
assert "hlelo".replace_first("le", "el") == "hello"
assert "hello".replace_first('l', "")    == "helo"
~~~

Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

Pull-Request: #2475

31 files changed:
contrib/shibuqam/examples/shibuqamoauth.nit
contrib/tinks/src/client/base.nit
contrib/tinks/src/common.nit
contrib/tinks/src/server/server.nit
lib/actors/actors.nit
lib/actors/examples/agent_simulation/agent_simulation.nit [new file with mode: 0644]
lib/actors/examples/agent_simulation/simple_simulation.nit [new file with mode: 0644]
lib/actors/examples/chameneos-redux/chameneosredux.nit
lib/gamnit/network/client.nit
lib/gamnit/network/common.nit
lib/gamnit/network/network.nit
lib/gamnit/network/server.nit
lib/popcorn/examples/angular/example_angular.nit
lib/popcorn/pop_auth.nit
lib/popcorn/pop_handlers.nit
lib/popcorn/pop_json.nit [new file with mode: 0644]
lib/popcorn/pop_repos.nit
lib/popcorn/pop_templates.nit
lib/popcorn/pop_tracker.nit
lib/popcorn/pop_validation.nit
lib/pthreads/concurrent_collections.nit
src/frontend/actors_generation_phase.nit
src/model/model.nit
src/modelize/modelize_class.nit
src/parser/parser_nodes.nit
src/web/web_base.nit
tests/sav/base_gen_final_bound.res [deleted file]
tests/sav/error_class_generic_alt2.res
tests/sav/test_gen_inh.res
tests/sav/test_super_gen.res
tests/sav/test_super_gen_raf.res

index 1aa33f9..326f0bf 100644 (file)
 module shibuqamoauth
 
 import popcorn
+import popcorn::pop_json
 import shibuqam
 import json
 
index bca5ae3..29f40c3 100644 (file)
@@ -35,22 +35,10 @@ redef class App
                else
                        print "Looking for a server..."
 
-                       var s = new UDPSocket
-                       s.enable_broadcast = true
-                       s.blocking = false
-                       s.broadcast(discovery_port, "Server? {handshake_app_name}")
-                       nanosleep(0, 100_000_000)
-
-                       var ptr = new Ref[nullable SocketAddress](null)
-                       var resp = s.recv_from(1024, ptr)
-                       var src = ptr.item
-
-                       if not resp.is_empty then
-                               var words = resp.split(" ")
-                               if words.length == 3 and words[0] == "Server!" and words[1] == handshake_app_name and words[2].is_numeric then
-                                       address = src.address
-                                       port = words[2].to_i
-                               end
+                       var servers = discover_local_servers
+                       if servers.not_empty then
+                               address = servers.first.address
+                               port = servers.first.port
                        end
                end
 
@@ -60,7 +48,7 @@ redef class App
                        # No command line
                        return new LocalServerContext
                else
-                       print "Connecting to:{address}:{port}"
+                       print "Connecting to {address}:{port}"
 
                        # Args are: tinks server_address {port}
                        if args.length > 1 then port = args[1].to_i
index b215d09..e7a82c9 100644 (file)
@@ -26,6 +26,5 @@ redef class Sys
        # Default listening port of the server
        fun default_listening_port: Int do return 18721
 
-       # Port to which clients send discovery requests
-       fun discovery_port: Int do return 18722
+       redef fun discovery_port do return 18722
 end
index 58555ae..69f2bdb 100644 (file)
@@ -26,13 +26,6 @@ redef class RemoteClient
 end
 
 redef class Server
-       # `UDPSocket` to which clients send discovery requests
-       var discovery_socket: UDPSocket do
-               var s = new UDPSocket
-               s.blocking = false
-               s.bind(null, discovery_port)
-               return s
-       end
 
        # The current game
        var game = new TGame is lazy, writable
@@ -62,24 +55,8 @@ redef class Server
                # Do game logic
                var turn = game.do_turn
 
-               # Respond to discovery requests
-               loop
-                       var ptr = new Ref[nullable SocketAddress](null)
-                       var read = discovery_socket.recv_from(1024, ptr)
-
-                       # No sender means there is no request (an error would also do it)
-                       var sender = ptr.item
-                       if sender == null then break
-
-                       var words = read.split(" ")
-                       if words.length != 2 or words[0] != "Server?" or words[1] != handshake_app_name then
-                               print "Server Warning: Rejected discovery request '{read}'"
-                               continue
-                       end
-
-                       discovery_socket.send_to(sender.address, sender.port,
-                               "Server! {handshake_app_name} {self.port}")
-               end
+               # Respond to discovery requests sent over UDP
+               answer_discovery_requests
 
                # Setup clients
                var new_clients = accept_clients
@@ -91,8 +68,6 @@ redef class Server
                        client.writer.serialize game
                        client.writer.serialize client.player
                        client.socket.flush
-
-                       clients.add client
                end
 
                if dedicated and clients.is_empty then
index 41ea816..d5e350f 100644 (file)
@@ -17,7 +17,7 @@ module actors is
        new_annotation actor
 end
 
-import pthreads::concurrent_collections
+intrude import pthreads::concurrent_collections
 intrude import pthreads
 intrude import pthreads::extra
 
@@ -34,24 +34,20 @@ abstract class Actor
        var instance: V
 
        # Mailbox used to receive and process messages
-       var mailbox = new BlockingQueue[Message].with_actor(self)
+       var mailbox = new Mailbox[Message].with_actor(self)
 
        # Is `self` working ?
        # i.e. does it have messages to process or is it processing one now ?
-       var working = false
+       var working = true
 
        redef fun main do
                loop
                        var m = mailbox.shift
                        if m isa ShutDownMessage then
-                               sys.active_actors.remove(self)
+                               sys.active_actors.decrement
                                return null
                        end
                        m.invoke(instance)
-                       if mailbox.is_empty then
-                               working = false
-                               sys.active_actors.remove(self)
-                       end
                end
        end
 
@@ -63,6 +59,64 @@ abstract class Actor
        end
 end
 
+# A Blocking queue implemented from a `ConcurrentList`
+# `shift` is blocking if there isn't any element in `self`
+# `push` or `unshift` releases every blocking threads
+# Corresponds to the mailbox of an actor
+class Mailbox[E]
+       super BlockingQueue[E]
+
+       # The associated actor
+       var actor: Actor is noautoinit
+
+       # init self with an associated actor
+       init with_actor(actor: Actor) do
+               self.actor = actor
+               sys.active_actors.increment
+       end
+
+       # Adding the signal to release eventual waiting thread(s)
+       redef fun push(e) do
+               mutex.lock
+               if real_collection.is_empty and not actor.working then
+                       actor.working = true
+                       sys.active_actors.increment
+                       real_collection.push(e)
+                       self.cond.signal
+               else
+                       real_collection.push(e)
+               end
+               mutex.unlock
+       end
+
+       redef fun unshift(e) do
+               mutex.lock
+               real_collection.unshift(e)
+               self.cond.signal
+               mutex.unlock
+       end
+
+       # If empty, blocks until an item is inserted with `push` or `unshift`
+       redef fun shift do
+               mutex.lock
+               if real_collection.is_empty then
+                       actor.working = false
+                       sys.active_actors.decrement
+                       while real_collection.is_empty do self.cond.wait(mutex)
+               end
+               var r = real_collection.shift
+               mutex.unlock
+               return r
+       end
+
+       redef fun is_empty do
+               mutex.lock
+               var r = real_collection.is_empty
+               mutex.unlock
+               return r
+       end
+end
+
 # A Message received by a Mailbox
 # In fact, this is the reification of a call
 # Each Message class represent a call to make on `instance` via `invoke`
@@ -151,91 +205,49 @@ class Future[E]
        end
 end
 
-# A Blocking queue implemented from a `ConcurrentList`
-# `shift` is blocking if there isn't any element in `self`
-# `push` or `unshift` releases every blocking threads
-# Corresponds to the mailbox of an actor
-class BlockingQueue[E]
-       super ConcurrentList[E]
-
-       # The associated actor
-       var actor: Actor is noautoinit
-
-       # Used to block or signal on waiting threads
-       private var cond = new PthreadCond
-
-       # init self with an associated actor
-       init with_actor(actor: Actor) do  self.actor = actor
-
-       # Adding the signal to release eventual waiting thread(s)
-       redef fun push(e) do
-               mutex.lock
-               if real_collection.is_empty and not actor.working then
-                       actor.working = true
-                       sys.active_actors.push(actor)
-               end
-               real_collection.push(e)
-               self.cond.signal
-               mutex.unlock
-       end
-
-       redef fun unshift(e) do
-               mutex.lock
-               real_collection.unshift(e)
-               self.cond.signal
-               mutex.unlock
-       end
+# A counter on which threads can wait until its value is 0
+class SynchronizedCounter
 
-       # If empty, blocks until an item is inserted with `push` or `unshift`
-       redef fun shift do
-               mutex.lock
-               while real_collection.is_empty do self.cond.wait(mutex)
-               var r = real_collection.shift
-               mutex.unlock
-               return r
-       end
-end
+       # The starting value, always starts with 0
+       private var c = 0
 
-# A collection which `is_empty` method blocks until it's empty
-class ReverseBlockingQueue[E]
-       super ConcurrentList[E]
-
-       # Used to block or signal on waiting threads
        private var cond = new PthreadCond
+       private var mutex = new Mutex
 
-       # Adding the signal to release eventual waiting thread(s)
-       redef fun push(e) do
+       # Increment the counter atomically
+       fun increment do
                mutex.lock
-               real_collection.push(e)
+               c += 1
                mutex.unlock
        end
 
-       # When the Queue is empty, signal any
-       # possible waiting thread
-       redef fun remove(e) do
+       # Decrement the counter atomically,
+       # signals to waiting thread(s) if `c == 0`
+       fun decrement do
                mutex.lock
-               real_collection.remove(e)
-               if real_collection.is_empty then cond.signal
+               c -= 1
+               if c == 0 then
+                       cond.signal
+               end
                mutex.unlock
        end
 
-       # Wait until the Queue is empty
-       redef fun is_empty do
+       # Block until `c == 0`
+       fun wait do
                mutex.lock
-               while not real_collection.is_empty do self.cond.wait(mutex)
+               while c != 0 do cond.wait(mutex)
                mutex.unlock
-               return true
        end
 end
 
 redef class Sys
 
-       # List of running actors
-       var active_actors = new ReverseBlockingQueue[Actor] is lazy
+       # Number of active actors
+       var active_actors = new SynchronizedCounter
 
        redef fun run do
                super
                # The program won't end until every actor is done
-               active_actors.is_empty
+               active_actors.wait
        end
 end
diff --git a/lib/actors/examples/agent_simulation/agent_simulation.nit b/lib/actors/examples/agent_simulation/agent_simulation.nit
new file mode 100644 (file)
index 0000000..35fe57c
--- /dev/null
@@ -0,0 +1,60 @@
+# 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.
+
+# a "Framework" to make Multi-Agent Simulations in Nit
+module agent_simulation is no_warning("missing-doc")
+
+import actors
+
+# Master of the simulation, it initiates the steps and waits for them to terminate
+class ClockAgent
+       actor
+
+       # Number of steps to do in the simulation
+       var nb_steps: Int
+
+       # List of Agents in the simulation
+       var agents: Array[Agent]
+
+       # Number of agents that finished their step
+       var nb_finished = 0
+
+       fun do_step do
+               for a in agents do a.async.do_step
+               nb_steps -= 1
+       end
+
+       fun finished_step do
+               nb_finished += 1
+               if nb_finished == agents.length then
+                       nb_finished = 0
+                       if nb_steps != 0 then async.do_step
+               end
+       end
+end
+
+class Agent
+       actor
+
+       # Doing a step in the simulation
+       fun do_step do
+       end
+
+       fun end_step do clock_agent.async.finished_step
+
+end
+
+redef class Sys
+       var clock_agent: ClockAgent is noautoinit,writable
+end
diff --git a/lib/actors/examples/agent_simulation/simple_simulation.nit b/lib/actors/examples/agent_simulation/simple_simulation.nit
new file mode 100644 (file)
index 0000000..d8be6c6
--- /dev/null
@@ -0,0 +1,57 @@
+# 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.
+
+# Using `agent_simulation` by refining the Agent class to make
+# a multi-agent simulation where every agent know each other
+# The steps consist of each agent greeting each other, and
+# waiting for every other agent to respond before notifying
+# to the `ClockAgent` that they finished their step.
+module simple_simulation
+
+import agent_simulation
+
+redef class Agent
+       var others = new Array[Agent]
+       var count = 0
+
+       fun greet(message: String, other: Agent) do other.async.greet_back("Hello back !")
+
+       fun greet_back(message: String) do
+               count -= 1
+               if count == 0 then end_step
+       end
+
+       redef fun do_step do
+               for o in others do
+                       o.async.greet("Hello !", self)
+                       count += 1
+               end
+       end
+end
+
+var nb_steps = 0
+var nb_agents = 0
+if args.is_empty or args.length != 2 then
+       nb_steps = 10
+       nb_agents = 10
+else
+       nb_steps = args[0].to_i
+       nb_agents = args[1].to_i
+end
+
+var agents = new Array[Agent]
+for i in [0..nb_agents[ do agents.add(new Agent)
+for a in agents do for b in agents do if a != b then a.others.add(b)
+clock_agent = new ClockAgent(nb_steps, agents)
+clock_agent.async.do_step
index 9143e22..60361a8 100644 (file)
@@ -128,7 +128,7 @@ fun work(n, nb_colors : Int ) do
 
        for c in creatures do c.async.run
 
-       active_actors.is_empty
+       active_actors.wait
 
        var total = 0
        for c in creatures do
index 38db2db..8aad96f 100644 (file)
@@ -40,7 +40,7 @@
 # ~~~
 module client
 
-import common
+intrude import common
 
 # Information of the remove server
 class RemoteServerConfig
@@ -126,3 +126,55 @@ class RemoteServer
                return true
        end
 end
+
+# Discover local servers responding on UDP `discovery_port`
+#
+# Sends a message in the format `gamnit::network? handshake_app_name` and
+# looks for the response `gamnit::network! handshake_app_name port_number`.
+# Waits for `timeout`, or the default 0.1 seconds, after sending the message.
+#
+# The server usually responds using the method `answer_discovery_requests`.
+# When receiving responses, the client may then choose a server and
+# connect via `new RemoteServer`.
+#
+# ~~~
+# var servers = discover_local_servers
+# if servers.not_empty then
+#     var server = new RemoteServer(servers.first)
+#     server.connect
+#     server.writer.serialize "hello server"
+#     server.socket.close
+# end
+# ~~~
+fun discover_local_servers(timeout: nullable Float): Array[RemoteServerConfig]
+do
+       timeout = timeout or else 0.1
+
+       var s = new UDPSocket
+       s.enable_broadcast = true
+       s.blocking = false
+       s.broadcast(discovery_port, "{discovery_request_message} {handshake_app_name}")
+       timeout.sleep
+
+       var r = new Array[RemoteServerConfig]
+       loop
+               var ptr = new Ref[nullable SocketAddress](null)
+               var resp = s.recv_from(1024, ptr)
+               var src = ptr.item
+
+               if resp.is_empty then
+                       # No response
+                       break
+               else
+                       assert src != null
+                       var words = resp.split(" ")
+                       if words.length == 3 and words[0] == discovery_response_message and
+                          words[1] == handshake_app_name and words[2].is_int then
+                               var address = src.address
+                               var port = words[2].to_i
+                               r.add new RemoteServerConfig(address, port)
+                       end
+               end
+       end
+       return r
+end
index 54e764a..579145b 100644 (file)
@@ -22,8 +22,9 @@ import binary::serialization
 #
 # This name must be the same between client/server and
 # it should not be used by other programs that may interfere.
-#
 # Both client and server refuse connections with a different name.
+#
+# This value must not contain spaces.
 fun handshake_app_name: String do return program_name
 
 # Version of the communication protocol to use in the handshake
@@ -32,4 +33,17 @@ fun handshake_app_name: String do return program_name
 # that different versions indicates incompatible protocols.
 #
 # Both client and server refuse connections with a different version.
+#
+# This value must not contain spaces.
 fun handshake_app_version: String do return "0.0"
+
+# Server port listening for discovery requests
+#
+# This name must be the same between client/server.
+fun discovery_port: Int do return 18722
+
+# First word in discovery requests
+private fun discovery_request_message: String do return "gamnit::network?"
+
+# First word in discovery responses
+private fun discovery_response_message: String do return "gamnit::network!"
index 83fd35d..93e1407 100644 (file)
 # limitations under the License.
 
 # Easy client/server logic for games and simple distributed applications
+#
+# Both `gamnit::client` and `gamnit::server` can be used separately or
+# together by importing `gamnit::network`.
+# Use both modules to create an program that discover local servers
+# or create one if none is found:
+#
+# ~~~
+# redef fun handshake_app_name do return "network_test"
+#
+# # Discover local servers
+# var servers = discover_local_servers
+# if servers.not_empty then
+#     # Try to connect to the first local server
+#     var server_info = servers.first
+#     var server = new RemoteServer(server_info)
+#
+#     if not server.connect then
+#         print_error "Failed to connect to {server_info.address}:{server_info.port}"
+#     else if not server.handshake then
+#         print_error "Failed handshake with {server_info.address}:{server_info.port}"
+#     else
+#         # Connected!
+#         print "Connected to {server_info.address}:{server_info.port}"
+#
+#         # Write something and close connection
+#         server.writer.serialize "hello server"
+#         server.socket.as(not null).close
+#     end
+# else
+#     # Create a local server
+#     var connect_port = 33729
+#     print "Launching server: connect on {connect_port}, discovery on {discovery_port}"
+#     var server = new Server(connect_port)
+#
+#     # Don't loop if testing
+#     if "NIT_TESTING".environ == "true" then exit 0
+#
+#     loop
+#         # Respond to discovery requests
+#         server.answer_discovery_requests
+#
+#         # Accept new clients
+#         var new_clients = server.accept_clients
+#         for client in new_clients do
+#             # Read something and close connection
+#             assert client.reader.deserialize == "hello server"
+#             client.socket.close
+#         end
+#     end
+# end
+# ~~~
 module network
 
 import server
index fcff7a2..70cf002 100644 (file)
 #
 #     # `accept_clients` in non-blocking,
 #     # sleep before tying again, or do something else.
-#     nanosleep(0, 50000000)
+#     0.5.sleep
 #     printn "."
 # end
 # ~~~
 module server
 
-import common
+intrude import common
 
 # Game server controller
 class Server
@@ -56,19 +56,24 @@ class Server
        # All connected `RemoteClient`
        var clients = new Array[RemoteClient]
 
-       # Socket accepting new connections
+       # TCP socket accepting new connections
+       #
+       # Opened on the first call to `accept_clients`.
        var listening_socket: TCPServer is lazy do
-               print port
                var socket = new TCPServer(port)
                socket.listen 8
                socket.blocking = false
                return socket
        end
-       init do listening_socket
 
        # Accept currently waiting clients and return them as an array
-       fun accept_clients: Array[RemoteClient]
+       #
+       # If `add_to_clients`, the default, the new clients are added to `clients`.
+       # Otherwise, the return value of `accept_clients` may be added to `clients`
+       # explicitly by the caller after an extra verification or sorting.
+       fun accept_clients(add_to_clients: nullable Bool): Array[RemoteClient]
        do
+               add_to_clients = add_to_clients or else true
                assert not listening_socket.closed
 
                var new_clients = new Array[RemoteClient]
@@ -87,17 +92,63 @@ class Server
                                client_socket.close
                        end
                end
+
+               if add_to_clients then clients.add_all new_clients
+
                return new_clients
        end
 
        # Broadcast a `message` to all `clients`, then flush the connection
-       fun broadcast(message: Serializable)
+       #
+       # The client `except` is skipped and will not receive the `message`.
+       fun broadcast(message: Serializable, except: nullable RemoteClient)
        do
-               for client in clients do
+               for client in clients do if client != except then
                        client.writer.serialize(message)
                        client.socket.flush
                end
        end
+
+       # Respond to pending discovery requests by sending the TCP listening address and port
+       #
+       # Returns the number of valid requests received.
+       #
+       # The response messages includes the TCP listening address and port
+       # for remote clients to connect with TCP using `connect`.
+       # These connections are accepted by the server with `accept_clients`.
+       fun answer_discovery_requests: Int
+       do
+               var count = 0
+               loop
+                       var ptr = new Ref[nullable SocketAddress](null)
+                       var read = discovery_socket.recv_from(1024, ptr)
+
+                       # No sender means there is no discovery request
+                       var sender = ptr.item
+                       if sender == null then break
+
+                       var words = read.split(" ")
+                       if words.length != 2 or words[0] != discovery_request_message or words[1] != handshake_app_name then
+                               print "Server Warning: Rejected discovery request '{read}' from {sender.address}:{sender.port}"
+                               continue
+                       end
+
+                       var msg = "{discovery_response_message} {handshake_app_name} {self.port}"
+                       discovery_socket.send_to(sender.address, sender.port, msg)
+                       count += 1
+               end
+               return count
+       end
+
+       # UDP socket responding to discovery requests
+       #
+       # Usually opened on the first call to `answer_discovery_request`.
+       var discovery_socket: UDPSocket is lazy do
+               var s = new UDPSocket
+               s.blocking = false
+               s.bind(null, discovery_port)
+               return s
+       end
 end
 
 # Reference to a remote client connected to this server
@@ -127,7 +178,7 @@ class RemoteClient
        # Check for compatibility with the client
        fun handshake: Bool
        do
-               print "Server: Handshake requested by {socket.address}"
+               print "Server: Handshake initiated by {socket.address}"
 
                # Make sure it is the same app
                var server_app = sys.handshake_app_name
index 9212a8e..e229a8c 100644 (file)
@@ -15,6 +15,7 @@
 # limitations under the License.
 
 import popcorn
+import popcorn::pop_json
 
 class CounterAPI
        super Handler
index 8039ba9..2f84be7 100644 (file)
@@ -77,6 +77,7 @@
 # ~~~
 module pop_auth
 
+import pop_json
 import pop_sessions
 import github
 
index 6b1e825..e657d68 100644 (file)
@@ -18,8 +18,6 @@
 module pop_handlers
 
 import pop_routes
-import json::static
-import json
 import csv
 
 # Class handler for a route.
@@ -450,16 +448,6 @@ redef class HttpResponse
                send(html, status)
        end
 
-       # Write data as JSON and set the right content type header.
-       fun json(json: nullable Serializable, status: nullable Int) do
-               header["Content-Type"] = media_types["json"].as(not null)
-               if json == null then
-                       send(null, status)
-               else
-                       send(json.to_json, status)
-               end
-       end
-
        # Write data as CSV and set the right content type header.
        fun csv(csv: nullable CsvDocument, status: nullable Int) do
                header["Content-Type"] = media_types["csv"].as(not null)
@@ -470,16 +458,6 @@ redef class HttpResponse
                end
        end
 
-       # Write error as JSON.
-       #
-       # Format: `{"message": message, "status": status}`
-       fun json_error(message: String, status: Int) do
-               var obj = new JsonObject
-               obj["status"] = status
-               obj["message"] = message
-               json(obj, status)
-       end
-
        # Redirect response to `location`
        #
        # Use by default 303 See Other as it is the RFC
diff --git a/lib/popcorn/pop_json.nit b/lib/popcorn/pop_json.nit
new file mode 100644 (file)
index 0000000..64a06e1
--- /dev/null
@@ -0,0 +1,185 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2017 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.
+
+# Introduce useful services for JSON REST API handlers.
+#
+# Validation and Deserialization of request bodies:
+#
+# ~~~nit
+# class MyJsonHandler
+#      super Handler
+#
+#      # Validator used do validate the body
+#      redef var validator = new MyFormValidator
+#
+#      # Define the kind of objects expected by the deserialization process
+#      redef type BODY: MyForm
+#
+#      redef fun post(req, res) do
+#              var post = validate_body(req, res)
+#              if post == null then return # Validation error: let popcorn return a HTTP 400
+#              var form = deserialize_body(req, res)
+#              if form == null then return # Deserialization error: let popcorn return a HTTP 400
+#
+#              # TODO do something with the input
+#              print form.name
+#      end
+# end
+#
+# class MyForm
+#      serialize
+#
+#      var name: String
+# end
+#
+# class MyFormValidator
+#      super ObjectValidator
+#
+#      init do
+#              add new StringField("name", min_size=1, max_size=255)
+#      end
+# end
+# ~~~
+module pop_json
+
+import json
+import pop_handlers
+import pop_validation
+
+redef class Handler
+
+       # Validator used to check body input
+       #
+       # Here we use the `pop_validation` module to validate JSON document from the request body.
+       var validator: nullable DocumentValidator = null
+
+       # Validate body input with `validator`
+       #
+       # Try to validate the request body as a json document using `validator`:
+       # * Returns the validated string input if the result of the validation is ok.
+       # * Answers a json error and returns `null` if something went wrong.
+       # * If no `validator` is set, returns the body without validation.
+       #
+       # Example:
+       #
+       # ~~~nit
+       # class ValidatedHandler
+       #       super Handler
+       #
+       #       redef var validator = new MyObjectValidator
+       #
+       #       redef fun post(req, res) do
+       #               var body = validate_body(req, res)
+       #               if body == null then return # Validation error
+       #               # At this point popcorn returned a HTTP 400 code with the validation error
+       #               # if the validation failed.
+       #
+       #               # TODO do something with the input
+       #               print body
+       #       end
+       # end
+       #
+       # class MyObjectValidator
+       #       super ObjectValidator
+       #
+       #       init do
+       #               add new StringField("name", min_size=1, max_size=255)
+       #       end
+       # end
+       # ~~~
+       fun validate_body(req: HttpRequest, res: HttpResponse): nullable String do
+               var body = req.body
+
+               var validator = self.validator
+               if validator == null then return body
+
+               if not validator.validate(body) then
+                       res.json(validator.validation, 400)
+                       return null
+               end
+               return body
+       end
+
+       # Deserialize the request body
+       #
+       # Returns the deserialized request body body or `null` if something went wrong.
+       # If the object cannot be deserialized, answers with a HTTP 400.
+       #
+       # See `BODY` and `new_body_object`.
+       #
+       # Example:
+       # ~~~nit
+       # class MyDeserializedHandler
+       #       super Handler
+       #
+       #       redef type BODY: MyObject
+       #
+       #       redef fun post(req, res) do
+       #               var form = deserialize_body(req, res)
+       #               if form == null then return # Deserialization error
+       #               # At this point popcorn returned a HTTP 400 code if something was wrong with
+       #               # the deserialization process
+       #
+       #               # TODO do something with the input
+       #               print form.name
+       #       end
+       # end
+       #
+       # class MyObject
+       #       serialize
+       #
+       #       var name: String
+       # end
+       # ~~~
+       fun deserialize_body(req: HttpRequest, res: HttpResponse): nullable BODY do
+               var body = req.body
+               var deserializer = new JsonDeserializer(body)
+               var form = deserializer.deserialize(body)
+               if not form isa BODY or deserializer.errors.not_empty then
+                       res.json_error("Bad input", 400)
+                       return null
+               end
+               return form
+       end
+
+       # Kind of objects returned by `deserialize_body`
+       #
+       # Define it in each sub handlers depending on the kind of objects sent in request bodies.
+       type BODY: Serializable
+end
+
+redef class HttpResponse
+
+       # Write data as JSON and set the right content type header.
+       fun json(json: nullable Serializable, status: nullable Int) do
+               header["Content-Type"] = media_types["json"].as(not null)
+               if json == null then
+                       send(null, status)
+               else
+                       send(json.to_json, status)
+               end
+       end
+
+       # Write error as JSON.
+       #
+       # Format: `{"message": message, "status": status}`
+       fun json_error(message: String, status: Int) do
+               var obj = new JsonObject
+               obj["status"] = status
+               obj["message"] = message
+               json(obj, status)
+       end
+end
index 3bdf2b0..2040c83 100644 (file)
@@ -37,6 +37,7 @@
 #
 # import popcorn
 # import popcorn::pop_repos
+# import popcorn::pop_json
 #
 # # Serializable book representation.
 # class Book
@@ -254,6 +255,7 @@ end
 # ~~~
 # import popcorn
 # import popcorn::pop_repos
+# import popcorn::pop_json
 #
 # # First, let's create a User abstraction:
 #
index f76c1e5..4aea76d 100644 (file)
@@ -83,6 +83,7 @@
 module pop_templates
 
 import popcorn::pop_handlers
+import popcorn::pop_json
 import template::macro
 
 redef class HttpResponse
index 048509e..9ecf5b7 100644 (file)
@@ -47,6 +47,7 @@ module pop_tracker
 import popcorn
 import popcorn::pop_config
 import popcorn::pop_logging
+import popcorn::pop_json
 import popcorn::pop_repos
 
 redef class AppConfig
index e95e517..ab92185 100644 (file)
@@ -24,6 +24,7 @@
 #
 # ~~~
 # import popcorn
+# import popcorn::pop_json
 # import serialization
 #
 # # Serializable book representation.
index e0c53af..3c5ae98 100644 (file)
@@ -508,3 +508,76 @@ class ConcurrentList[E]
                return value
        end
 end
+
+# A collection which `is_empty` method blocks until it's empty
+class ReverseBlockingQueue[E]
+       super ConcurrentList[E]
+
+       # Used to block or signal on waiting threads
+       private var cond = new PthreadCond
+
+       # Adding the signal to release eventual waiting thread(s)
+       redef fun push(e) do
+               mutex.lock
+               real_collection.push(e)
+               mutex.unlock
+       end
+
+       # When the Queue is empty, signal any possible waiting thread
+       redef fun remove(e) do
+               mutex.lock
+               real_collection.remove(e)
+               if real_collection.is_empty then cond.signal
+               mutex.unlock
+       end
+
+       # Wait until the Queue is empty
+       redef fun is_empty do
+               mutex.lock
+               while not real_collection.is_empty do self.cond.wait(mutex)
+               mutex.unlock
+               return true
+       end
+end
+
+# A Blocking queue implemented from a `ConcurrentList`
+# `shift` is blocking if there isn't any element in `self`
+# `push` or `unshift` releases every blocking threads
+class BlockingQueue[E]
+       super ConcurrentList[E]
+
+       # Used to block or signal on waiting threads
+       private var cond = new PthreadCond
+
+       # Adding the signal to release eventual waiting thread(s)
+       redef fun push(e) do
+               mutex.lock
+               real_collection.push(e)
+               self.cond.signal
+               real_collection.push(e)
+               mutex.unlock
+       end
+
+       redef fun unshift(e) do
+               mutex.lock
+               real_collection.unshift(e)
+               self.cond.signal
+               mutex.unlock
+       end
+
+       # If empty, blocks until an item is inserted with `push` or `unshift`
+       redef fun shift do
+               mutex.lock
+               while real_collection.is_empty do self.cond.wait(mutex)
+               var r = real_collection.shift
+               mutex.unlock
+               return r
+       end
+
+       redef fun is_empty do
+               mutex.lock
+               var r = real_collection.is_empty
+               mutex.unlock
+               return r
+       end
+end
index b67d390..eaf8153 100644 (file)
@@ -15,6 +15,7 @@
 # See `nit/lib/actors/actors.nit` for the abstraction on which the generated classes are based
 module actors_generation_phase
 
+import actors_injection_phase
 import modelize
 import gen_nit
 import modelbuilder
@@ -22,6 +23,8 @@ import modelbuilder
 private class ActorPhase
        super Phase
 
+       var generated_actor_modules = new Array[String]
+
        # Source code of the actor classes to generate
        var actors = new Array[String]
 
@@ -34,27 +37,14 @@ private class ActorPhase
        # Redefinitions of annotated classes
        var redef_classes = new Array[String]
 
-       redef fun process_annotated_node(nclass, nat)
-       do
-               if nat.n_atid.n_id.text != "actor" then return
-
-               if not nclass isa AStdClassdef then
-                       toolcontext.error(nat.location, "Syntax Error: only a class can be annotated as an actor.")
-                       return
-               end
-
-               # Get the module associated with this class
-               var mclassdef = nclass.mclassdef
-               assert mclassdef != null
-
-               var mmod = mclassdef.mmodule
+       fun generate_actor_classes(mclassdef: MClassDef, mmod: MModule) do
                if not mmod.generate_actor_submodule then mmod.generate_actor_submodule = true
 
                # Get the name of the annotated class
                var classname = mclassdef.name
 
                # Generate the actor class
-               actors.add(
+               if mclassdef.is_intro then actors.add(
 """
 class Actor{{{classname}}}
        super Actor
@@ -64,7 +54,11 @@ end
                ######## Generate the Messages classes ########
 
                # Get all the methods definitions
-               var propdefs = mclassdef.mpropdefs
+               var propdefs = new Array[MPropDef]
+               for propdef in mclassdef.mpropdefs do
+                       if propdef.is_intro then propdefs.add(propdef)
+               end
+
                var methods = new Array[MMethodDef]
                for p in propdefs do
                        if p isa MMethodDef then
@@ -77,7 +71,8 @@ end
 
                # Generate the superclass for all Messages classes (each actor has its own Message super class)
                var msg_class_name = "Message" + classname
-               messages.add(
+
+               if mclassdef.is_intro then messages.add(
 """
 class {{{msg_class_name}}}
        super Message
@@ -173,12 +168,14 @@ end
                # All of the functions of the proxy too
 
                # Let's generate the proxy class then
+
+               var redef_virtual_type = ""
+               if mclassdef.is_intro then redef_virtual_type = "redef type E: Actor{classname}"
                proxys.add(
 """
 redef class Proxy{{{classname}}}
 
-       redef type E: Actor{{{classname}}}
-       #var actor: Actor{{{classname}}} is noinit
+       {{{redef_virtual_type}}}
 
        init proxy(base_class: {{{classname}}}) do
                actor = new Actor{{{classname}}}(base_class)
@@ -189,12 +186,46 @@ redef class Proxy{{{classname}}}
 end
 """)
 
-               redef_classes.add(
+       if mclassdef.is_intro then redef_classes.add(
 """
 redef class {{{classname}}}
-redef var async is lazy do return new Proxy{{{classname}}}.proxy(self)
+       var m = new Mutex
+       var lazy_proxy: Proxy{{{classname}}} is lazy do return new Proxy{{{classname}}}.proxy(self)
+
+       redef fun async: Proxy{{{classname}}} do
+               m.lock
+               var p = lazy_proxy
+               m.unlock
+               return p
+       end
 end
 """)
+
+       end
+
+       redef fun process_nmodule(nmodule) do
+               var mmod = nmodule.mmodule
+               if mmod == null then return
+
+               if generated_actor_modules.has(mmod.name) then return
+
+               var mclasses_defs = mmod.mclassdefs
+               for mclass_def in mclasses_defs do
+                       var mclass = mclass_def.mclass
+                       var actor = mclass.actor
+                       if actor != null then generate_actor_classes(mclass_def, mmod)
+               end
+
+       end
+
+       redef fun process_annotated_node(nclass, nat)
+       do
+               if nat.n_atid.n_id.text != "actor" then return
+
+               if not nclass isa AStdClassdef then
+                       toolcontext.error(nat.location, "Syntax Error: only a class can be annotated as an actor.")
+                       return
+               end
        end
 
        redef fun process_nmodule_after(nmodule) do
@@ -226,6 +257,10 @@ end
                # for mmod in mmodules do nit_module.imports.add mmod.name
                nit_module.imports.add first_mmodule.name
 
+               generated_actor_modules.add(module_name)
+               var idx = generated_actor_modules.index_of(module_name)
+               for i in [0..idx[ do nit_module.imports.add(generated_actor_modules[i])
+
                nit_module.content.add "####################### Redef classes #########################"
                for c in redef_classes do nit_module.content.add( c + "\n\n" )
 
@@ -241,10 +276,10 @@ end
                # Write support module
                nit_module.write_to_file module_path
 
-               actors = new Array[String]
-               messages = new Array[String]
-               proxys = new Array[String]
-               redef_classes = new Array[String]
+               actors.clear
+               messages.clear
+               proxys.clear
+               redef_classes.clear
 
                toolcontext.modelbuilder.inject_module_subimportation(first_mmodule, module_path)
        end
@@ -257,5 +292,5 @@ end
 
 redef class ToolContext
        # Generate actors
-       var actor_phase: Phase = new ActorPhase(self, [modelize_class_phase])
+       var actor_phase: Phase = new ActorPhase(self, [modelize_class_phase, modelize_property_phase])
 end
index 21b182e..af53e3d 100644 (file)
@@ -2430,11 +2430,9 @@ abstract class MPropDef
                                        res.append "::"
                                end
                        end
-                       if mclassdef.mclass != mproperty.intro_mclassdef.mclass then
-                               # precise "B" only if not the same class than "A"
-                               res.append mproperty.intro_mclassdef.name
-                               res.append "::"
-                       end
+                       # precise "B" because it is not the same class than "A"
+                       res.append mproperty.intro_mclassdef.name
+                       res.append "::"
                        # Always use the property name "x"
                        res.append mproperty.name
                end
@@ -2452,10 +2450,8 @@ abstract class MPropDef
                                res.append mproperty.intro_mclassdef.mmodule.c_name
                                res.append "__"
                        end
-                       if mclassdef.mclass != mproperty.intro_mclassdef.mclass then
-                               res.append mproperty.intro_mclassdef.name.to_cmangle
-                               res.append "__"
-                       end
+                       res.append mproperty.intro_mclassdef.name.to_cmangle
+                       res.append "__"
                        res.append mproperty.name.to_cmangle
                end
                return res.to_s
index 8164668..53885ad 100644 (file)
@@ -188,9 +188,6 @@ redef class ModelBuilder
                                                bounds.add(bound)
                                                nfd.bound = bound
                                        end
-                                       if bound isa MClassType and bound.mclass.kind == enum_kind then
-                                               warning(nfdt, "useless-bound", "Warning: useless formal parameter type since `{bound}` cannot have subclasses.")
-                                       end
                                else if mclass.mclassdefs.is_empty then
                                        if objectclass == null then
                                                error(nfd, "Error: formal parameter type `{pname}` unbounded but no `Object` class exists.")
index bcbacb3..dc3b1f7 100644 (file)
@@ -22,7 +22,7 @@ private import console
 
 # Root of the AST class-hierarchy
 abstract class ANode
-       # Location is set during AST building. Once built, location cannon be null.
+       # Location is set during AST building. Once built, location can not be null.
        # However, manual instantiated nodes may need more care.
        var location: Location is writable, noinit
 
@@ -534,7 +534,7 @@ class TKwinterface
        super TokenKeyword
 end
 
-# The keywords `enum` ane `universal`
+# The keywords `enum` and `universal`
 class TKwenum
        super TokenKeyword
 end
@@ -3023,7 +3023,8 @@ abstract class AAtid
        super Prod
 
        # The identifier of the annotation.
-       # Can be a TId of a keyword
+       #
+       # Can be a TId or a keyword.
        var n_id: Token is writable, noinit
 end
 
index 8062768..265cadf 100644 (file)
@@ -21,6 +21,7 @@ import doc_down
 import popcorn
 import popcorn::pop_config
 import popcorn::pop_repos
+import popcorn::pop_json
 
 # Nitweb config file.
 class NitwebConfig
diff --git a/tests/sav/base_gen_final_bound.res b/tests/sav/base_gen_final_bound.res
deleted file mode 100644 (file)
index a0d7579..0000000
+++ /dev/null
@@ -1 +0,0 @@
-base_gen_final_bound.nit:23,16--18: Warning: useless formal parameter type since `Int` cannot have subclasses.
index 31fe7ed..ef0f1c9 100644 (file)
@@ -1,2 +1 @@
-alt/error_class_generic_alt2.nit:18,22--26: Warning: useless formal parameter type since `Float` cannot have subclasses.
 alt/error_class_generic_alt2.nit:25,8--12: Type Error: `Array[E: nullable Object]` is a generic class.
index a1e0a96..b48ebc3 100644 (file)
@@ -1,5 +1,3 @@
-test_gen_inh.nit:29,15--17: Warning: useless formal parameter type since `Int` cannot have subclasses.
-test_gen_inh.nit:34,15--17: Warning: useless formal parameter type since `Int` cannot have subclasses.
 11
 22
 33
index 78c1dcc..be0358a 100644 (file)
@@ -1,4 +1,3 @@
-test_super_gen.nit:27,12--14: Warning: useless formal parameter type since `Int` cannot have subclasses.
 1
 0
 5
index 1fc76c7..b2f1f1e 100644 (file)
@@ -1,4 +1,2 @@
-test_super_gen.nit:27,12--14: Warning: useless formal parameter type since `Int` cannot have subclasses.
-test_super_gen_raf.nit:19,12--14: Warning: useless formal parameter type since `Int` cannot have subclasses.
 0
 20