module Cute

Overview

Easy to use event-oriented publisher/subscribe modelled after the Qt Framework.

Defined in:

cute.cr
cute/buffer_sink.cr
cute/interval_sink.cr
cute/signal.cr
cute/sink.cr
cute/timed_buffer_sink.cr
cute/version.cr
spec.cr

Constant Summary

VERSION = "0.4.0"

Macro Summary

Macro Detail

macro connect(signal, handler) #

Connects a method as handler for a signal. The method must be given as prototype. The argument types are not important though, feel free to omit them. Returns the signal handler.

Example usage:

class Window
  def initialize
    @btn = Button.new
    @btn_handler = Cute.connect @btn.clicked, on_button_clicked(x, y)
  end

  def on_button_clicked(x, y)
    # ...
  end
end

See samples/qt_connect.cr for a runnable example.

Note: The handler may be called in a different Fiber.


[View source]
macro middleware(deff) #

Creates a middleware. A middleware offers the user of your class to extend its functionality by adding user-defined behaviour before calling the actualy middleware, giving the opportunity to modify arguments or the result value, or not call later stages at all.

The argument to the macro is a full method definition, whose code body will be called last. It's named the "final stage" because of that, and always exists.

Like Cute.signal, the macro creates a class and makes the method return it. You can then #add middleware, get the #list of added middleware and modify it directly, or #call it.

Example usage:

class Chat
  Cute.middleware def send_message(body : String) : String
    # `self` is the instance of `Chat` here.
    puts "Sending #{body}"
    "Sent #{body.size} bytes"
  end
end

chat = Chat.new
chat.send_message.add { |body, yielder| yielder.call body.upcase }

chat.send_message.call("Hello") # => "Sent 5 bytes"
# Prints "Sending HELLO"

Calling behaviour

When called (Through #call), the algorithm will run each middleware stage in the order it was added (Or: As it appears in the #list). That is, the stage that was added first will be called first, the second one after that, and finally the final stage.

It's possible to manually add or reorder the middleware stages by modifying the #list directly.

The final stage is called in the context of the host class instance. That means it behaves like a normal method, as if it was declared without the macro.

Caveats

For this to work you have to explicitly mark the argument and result types. Also note that if your middleware decides to not call the next stage, you still have to return something matching the return type. You can make it easier by allowing nil results.

This won't work:

# Swear filter
chat.send_message.add { |body, yielder| yielder.call(body) if body !~ /dang/ }

Possible solutions are these:

# Solution 1: Return something matching the return type
chat.send_message.add do |body, yielder|
  if body !~ /dang/
    yielder.call(body)
  else
    "Swearing is not allowed"
  end
end

# Solution 2: Allow nil result
class Chat
  Cute.middleware def send_message(body : String) : String?
    # ...
  end
end

# Now the previous example would work fine:
chat.send_message.add { |body, yielder| yielder.call(body) if body !~ /dang/ }

[View source]
macro signal(call, async = false) #

Creates a signal. A signal manages listeners (So called slots), which will be called when an event has been emitted. Emitting is the act of triggering a signal to announce an event to the listeners.

Creates the method name, which returns a Signal. This method does not take any arguments. Instead, the call arguments are type declarations for the signal arguments, and will be passed through Signal#emit to listeners as block arguments in the same order.

By default, the signal listeners are called in the fiber #emit is called from. You can override this by setting the optional argument async to true. In this case, the signal listeners will be called in turn in a Fiber on their own.

Example usage:

class Button
  Cute.signal clicked(x : Int32, y : Int32)
end

btn = Button.new
btn.clicked.on { |x, y| p x, y }
btn.clicked.emit 5, 4 # => Will print 5, 4

Note: You have to fully qualify all argument types to Cute.signal.

If your signal does not require any arguments, you can omit the ():

class MyIO
  Cute.signal data_received() # With empty parantheses
  Cute.signal closed          # Or without. Both are fine.
end

Signal listeners are called in the order they were connected. You can disconnect them later by using Signal#disconnect. Pass it the identifier you got from #on:

btn = Button.new
handle = btn.clicked.on { |x, y| p x, y }
btn.clicked.disconnect(handle) # Removes the connection again.

It's also possible to use a Channel instead, if you want to wait for an event in a blocking fashion. Use the #new_channel method for this:

btn = Button.new
ch, handle = btn.clicked.new_channel # Create channel
x, y = ch.retrieve                   # Wait for event
btn.clicked.disconnect(handle)       # Remove channel

The channel will transport the signal argument, or will use a Tuple if the signal uses multiple arguments. If the signal has no arguments, the channel will be Channel(Nil) instead.

Note: A channel is just like a handle. Make sure to disconnect it if you don't need it any longer.

You can also wait just once for an event using the #wait method. This method returns the signal arguments. Example:

btn = Button.new
x, y = btn.clicked.wait # Wait just once

This is especially useful if you're only interested in the event once or rarely. If you expect events regularly, you're better served using one of the other solutions above.

Look in samples/ for runnable examples.

Collecting signal emissions

Have a look at Cute::Sink, and its documentation, on how you can collect multiple emissions. This is also interesting to synchronize for batch-processing, making it easy to manage asynchronous dependencies.

Naming convention

Signal names should be written in past-tense: The slots are usually called when something already happened. So, a signal name like clicked is good, a signal name like click not so much. This also helps avoiding name clashes.

Testing

Using Cute.spy, you can create signal spies, which will record all emitted values. Please have a look at its documentation for further information.


[View source]
macro spy(sender, signal) #

Builds a signal spy listening on sender for signal. signal is the signal definition, like in Cute.signal. Types must not be omitted. Whenever a signal occurs, its value(s) are appended to the signal spy.

Full usage example:

require "cute/spec" # Or put it into your "spec_helper" file

class Button
  Cute.signal clicked(x : Int32, y : Int32)

  def click
    clicked.emit(4, 5)
  end
end

describe Button do
  describe "#click" do
    it "emits #clicked" do
      btn = Button.new
      clicked_spy = Cute.spy btn, clicked(x : Int32, y : Int32) # Create the spy
      btn.click                                                 # Somehow emit the spied upon signal
      clicked_spy.size.should eq 1                              # Verify
      clicked_spy[0].should eq({4, 5})
    end
  end
end

Note: Signals without arguments add nil into the spy.


[View source]