Let's Code a bit

Before we start

Busit supports multiple programming languages for BusApps: PHP 7, Python 3 and Java 8.
It is highly recommended that you'd be familiar with OOP.

  • BusApps are stateless and must have a parameterless constructor, see lifecycle below for more details.
  • BusApps are composed of at least one class (and ideally only one).
  • BusApps must be validated by Busit prior to be available on the platform.
  • All programming languages supported share the same object model (as close as possible), this may lead to unusual methods or structures for some languages but allows to have a common meaningful documentation for all programming languages.
  • For most accurate class documentation, see the javadoc
  • You should define the BusApp's capabilities and settings, name, description, translations, what channels it supports, what are the configuration parameters,...This is done via the configuration file. Then, you can write the actual code of your BusApp according to the settings you have declared.

1) Basic structure

There are 2 basic requirements for a BusApp:

  1. You must define the name of the BusApp class to be instanciated.
  2. PHP Python Java
    
    define("__CLASSNAME__", "\\HelloWorld");
    
    
    __CLASSNAME__ = "HelloWorld"
    
    
    public class BusApp { 
    	public static final String __CLASSNAME__ = "HelloWorld";
    }
    
  3. You must extend the abstract class App and implement (at least) one of the Producer, Consumer or Transformer patterns.
  4. PHP Python Java
    
    use com\busit\App;
    use com\busit\Producer;
    
    class HelloWorld extends App implements Producer
    {
    	...
    }
    
    
    from com.busit import App
    from com.busit import Producer
    
    class HelloWorld(App, Producer):
    	...
    
    
    import com.busit.App;
    import com.busit.Producer;
    
    public class HelloWorld extends App implements Producer { 
    	...
    }
    

The base App class implements a bunch of behaviors to help you:

PHP Python Java

namespace com\busit;

class App
{
	public function config($key=null); 	// returns the matching config parameter (or all if $key == null)
	
	public function id(); 		// returns a unique id for the user instance of the BusApp
	public function locale(); 	// returns the preferred language of the user
	public function name(); 	// returns the user-given name of the instance
	
	public function notifyUser($message); 		// send warnings to the user (use sparsely)
	public function notifyOwner($message, $data); 	// send warnings to the developper (aka: you) (use sparsely)
}

class App:
	def config(this, _key=None): pass # returns the matching config parameter (or all if _key == None)
	
	def id(this): pass 	# returns a unique id for the user instance of the BusApp
	def locale(this): pass 	# returns the preferred language of the user
	def name(this): pass 	# returns the user-given name of the instance

	def notifyUser(this, _message): pass 	# send warnings to the user (use sparsely)
	def notifyOwner(this, _message, _data=None): pass 	# send warnings to the developper (aka: you) (use sparsely)

package com.busit;

public abstract class App {
	public String config(String key); 	// returns the matching config parameter as a string
	public Any config(); 			// returns all the config parameters

	public String id(); 		// returns a unique id for the user instance of the BusApp
	public String locale(); 	// returns the preferred language of the user
	public String name(); 		// returns the user-given name of the instance
	
	public void notifyUser(String message); 		// send warnings to the developper (aka: you) (use sparsely)
	public void notifyOwner(String message, Object data); 	// send warnings to the developper (aka: you) (use sparsely)
}

2) Lifecycle

Everytime a BusApp is triggered, a new instance of the class is created. After processing the message, the class instance is destroyed. This means that there is no persistency nor any kind of state of the class once the lifecycle is complete: BusApps are stateless.

If your BusApp absolutely requires an internal state or some sort of history, you should setup one of your own using external services or use the internal SQL or NoSQL storage available through the Factory class.

  1. The class specified by the __CLASSNAME__ constant is instanciated using a parameterless constructor.
  2. If you implemented the Consumer pattern, then the consume() method is called once for the targetted channel only.
  3. The Busit broker checks if there are any other BusApps in the flow ; if there are any:
    • If you implemented the Producer pattern, then the produce() method is called once for each connected output.
    • If you implemented the Transformer pattern, then the transform() method is called once for each connected output.
  4. Your BusApp's class instance is destroyed

This means that internal variables will be maintained between consume() and subsequent produce() or transform() calls.

Producer pattern

If your BusApp brings data from external devices or services, it is producing data for Busit.
External devices or services can push data directly on your BusApp, or your BusApp can pull from those at regular intervals (see automatic channels).
In this case, you should implement the Producer interface.

PHP Python Java

namespace com\busit;

interface Producer
{
	public function produce($out, $data=null);
	public function sample($out);
	public function test();
}

class Producer:
	def produce(this, _out, _data=None): pass
	def sample(this, _out): pass
	def test(this): pass

package com.busit;

public interface Producer {
	public Message produce(String out, Message data) throws Exception;
	public Message sample(String out) throws Exception;
	public boolean test() throws Exception;
}

produce()

  • The out parameter is the name of the output channel that should produce data. More about Channels.
  • The data parameter is an optional Message instance that contains the pushed data.
  • The method must return a Message or null if there is no content to be produced.

sample()

  • The out parameter is the name of the output channel for which to get sample data. More about Channels.
  • The method should return a Message or null if there no data can be produced from that channel.

test()

  • The method should return a boolean value indicating if the BusApp is working properly. This is where you should check any possible dependency.
PHP Python Java

<?php
define("__CLASSNAME__", "\\WeatherStation");
use com\busit\App;
use com\busit\Producer;
use com\busit\Message;

class WeatherStation extends App implements Producer
{
	public function produce($out, $data=null)
	{
		$message = new Message();
		$content = $message->content();
		if( $out == 'pressure' )
			content['data'] = "38psi";
		else if( $out == 'temperature' )
			content['data'] = "-14 C";
			
		return $message;
	}
	
	public function sample($out)
	{
		$message = new Message();
		$content = $message->content();
		content['data'] = 42;
		
		return $message;
	}
	
	public function test()
	{
		return true;
	}
}
?>

__CLASSNAME__ = "WeatherStation"
from com.busit import App
from com.busit import Producer
from com.busit import Message

class WeatherStation(App, Producer):
	def produce(this, _out, _data=None):
		message = new Message()
		content = message.content()
		if _out == 'pressure':
			content['data'] = "38psi"
		elif _out == 'temperature':
			content['data'] = "-14 C"
		
		return message
	
	def sample(this, _out):
		message = new Message()
		content = message.content()
		content['data'] = 42
		
		return message
	
	def test():
		return True

public class BusApp { 
	public static final String __CLASSNAME__ = "WeatherStation";
}

import com.busit.App;
import com.busit.Producer;
import com.busit.Message;

public class WeatherStation extends App implements Producer {

	public Message produce(String out, Message data) throws Exception {
		Message message = new Message();
		Content content = message.content();
		if( out.equals("pressure") )
			content.put("data", "38psi");
		else if( out.equals("temperature") )
			content.put("data", "-14 C");
		
		return message;
	}
	
	public Message sample(String out) throws Exception {
		Message message = new Message();
		Content content = message.content();
		content.put("data", 42);
		
		return message;
	}
	
	public boolean test() throws Exception {
		return true;
	}
}

Consumer pattern

If your BusApp sends data to external devices or services, it is consuming data from Busit and acts as an end point.
For this purpose, you should implement the Consumer interface.

PHP Python Java

namespace com\busit;

interface Consumer
{
	public function consume($message, $in);
	public function test();
}

class Consumer:
	def consume(this, _message, _in): pass
	def test(this): pass

package com.busit;

public interface Consumer {
	public void consume(Message message, String in) throws Exception;
	public boolean test() throws Exception;
}

consume()

  • The message parameter is of type Message and contains the actual data. More about Messages.
  • The in parameter is the name of the input channel to which data should be sent. More about Channels.
  • The method does not return anything.

test()

  • The method should return a boolean value indicating if the BusApp is working properly. This is where you should check any possible dependency.
PHP Python Java

<?php
define("__CLASSNAME__", "\\Light");
use com\busit\App;
use com\busit\Consumer;

class Light extends App implements Consumer
{
	public function consume($mssage, $in)
	{
		if( $in == 'on' )
			$this->turnOn();
		else if( $in == 'off' )
			$this->turnOff();
		else
			$this->changeColor($message->content()['color']);
	}
	
	public function test()
	{
		return true;
	}
	
	private function turnOn() {}
	private function turnOff() {}
	private function changeColor($color) {}
}
?>

__CLASSNAME__ = "Light"
from com.busit import App
from com.busit import Consumer

class Light(App, Consumer):
	def consume(this, _message, _in):
		if _in == 'on':
			this.turnOn()
		elif _in == 'off':
			this.turnOff()
		else
			this.changeColor(_message.content()['color'])
	
	def test():
		return True
	
	def turnOn(this): pass
	def turnOff(this): pass
	def changeColor(this, _color): pass

public class BusApp { 
	public static final String __CLASSNAME__ = "Light";
}

import com.busit.App;
import com.busit.Consumer;

public class Light extends App implements Consumer {

	public Message consume(Message message, String in) throws Exception {
		if( in.equals("on") )
			this.turnOn();
		else if( in.equals("off") )
			this.turnOff();
		else
			this.changeColor(message.content().asString('color'));
	}
	
	public boolean test() throws Exception {
		return true;
	}
	
	private void turnOn() {}
	private void turnOff() {}
	private void changeColor(String color) {}
}

Transformer pattern

If your BusApp aims at modifying or enhancing data eventually using external services, it is transforming data for Busit.
In this case, you should implement the Transformer interface.

PHP Python Java

namespace com\busit;

interface Transformer
{
	public function transform($message, $in, $out);
	public function test();
}

class Transformer:
	def transform(this, _message, _in, _out): pass
	def test(this): pass

package com.busit;

public interface Transformer {
	public Message transform(Message message, String in, String out) throws Exception;
	public boolean test() throws Exception;
}

transform()

  • The message parameter is of type Message and contains the actual data. More about Messages.
  • The in parameter is the name of the input channel and gives you a hint on what data is comming in. More about Channels.
  • The out parameter is the name of the output channel and tells you what the user wants to come out. More about Channels.
  • The method must return a Message or null if you want to stop the flow and prevent further processing.

test()

  • The method should return a boolean value indicating if the BusApp is working properly. This is where you should check any possible dependency.
PHP Python Java

<?php
define("__CLASSNAME__", "\\Translator");
use com\busit\App;
use com\busit\Transformer;

class Translator extends App implements Transformer
{
	public function transform($mssage, $in, $out)
	{
		$content = $message->content();
		if( $in == 'fr' && $out == 'en' )
			$content['text'] = $this->fr2en($content['text']);
		else if( $in == 'en' && $out == 'fr' )
			$content['text'] = $this->en2fr($content['text']);
		
		return $message;
	}
	
	public function test()
	{
		return true;
	}
	
	private function fr2en($text) {}
	private function en2fr($text) {}
}
?>

__CLASSNAME__ = "Translator"
from com.busit import App
from com.busit import Transformer

class Translator(App, Transformer):
	def transform(this, _message, _in, _out):
		content = _message.content()
		if _in == 'fr' and _out == 'en':
			content['text'] = this.fr2en(content['text'])
		elif _in == 'en' and _out == 'fr':
			content['text'] = this.en2fr(content['text'])
		
		return _message
	
	def test():
		return True
	
	def fr2en(this, _text): pass
	def en2fr(this, _text): pass

public class BusApp { 
	public static final String __CLASSNAME__ = "Translator";
}

import com.busit.App;
import com.busit.Transformer;

public class Translator extends App implements Transformer {

	public Message transform(Message message, String in, String out) throws Exception {
		Content content = message.content();
		if( in.equals("fr") && out.equals("en") )
			content.put("text", this.fr2en(content.asString("text")));
		else if( in.equals("en") && out.equals("fr") )
			content.put("text", this.en2fr(content.asString("text")));
		
		return message;
	}
	
	public boolean test() throws Exception {
		return true;
	}
	
	private String fr2en(String text) {}
	private String en2fr(String text) {}
}

About messages

The Message conveys data across the BusApps and make sure the content is encrypted with the user's identity... hopefully, this is all transparent for you.

1) Message

Messages are composed of the following elements:

  • The content exposed as a Content structure
  • The files that can contain pictures, attachments,...

interface Message
{
	public function file($name, $data = null); // Get or Set a file for the message
	public function files();		// Get all files attached to the message
	
	public function content($data = null); 	// Get or Set the content of the message
	
	public function clear(); 		// Clears the content and files of the message
	public function copy(); 		// Get a shallow copy of the message (files content is referenced and not copied)
}

You can obtain a new Message instance using the Factory:


$message = com.busit.Factory::message();

2) MessageList

In case you want to return multiple messages at once, you can use a MessageList which is a wrapper around Message that behaves just like an array. Do not try to use it as an Message because it will throw at you... Simply append other messages to it.
Note that nested MessageList is not supported.


interface MessageList extends Message, ArrayAccess, Countable, IteratorAggregate
{
}

You can obtain a new MessageList instance using the Factory:


$list = com.busit.Factory::messageList();

$list[] = $message1;
$list[] = $message2;

3) Content

The actual content of the message is a very flexible structure that allows developers to strenghten compatibility accross BusApps. See it as a kind of high-level Mime type for data. A extensible set of predefined content types is available so that everyone can share common data structures. In the end, it behaves like an array in which you can put arbitrary key/value pairs.

Content has:

  • An id that uniquely identifies the type of content
  • A friendly name because it is always better than an id... but you should not rely on it !
  • A list of compatibility to share common values as another existing type
  • A text format template used to represent this content as simple text
  • A html format template used to represent this content as html

interface Content
{
	public function id($value = null); 		// Get or Set the id
	public function name($value = null);		// Get or Set the name
	
	public function compatible($id, $value = null); // Get or Set the compatibility with another content type
	public function compatibility($list = null); 	// Get or Set the list of compatible content types
	
	public function textFormat($value = null); 	// Get or Set the text format template
	public function toText($format = null); 	// Apply the text format to the data
	
	public function htmlFormat($value = null); 	// Get or Set the text format template
	public function toHtml($format = null); 	// Apply the html format to the data
	
	public function toJson(); 			// Get a json representation of this entire content type including data
	public function merge($content); 		// import another content's data into this one
}

You can obtain a new Content instance using the Factory:


$content = com.busit.Factory::content(42); // get content type with id 42
$content['answer'] = 42;
$content['question'] = 'undefined';

For more info about content types compatibility, text and html formats,... See deep into message content.

About channels (inputs and outputs)

Channels are either inputs or outputs from your BusApp. When publishing your BusApp, you should mention the different inputs and outputs that your BusApp accepts and give each of them an internal name (the channel key) that will never change.

When one of the produce(), consume() or transform() function is called, the concerned channel name will be provided such that the apropriate action can be taken.


<?php
define("__CLASSNAME__", "\\YesNoFilter");

class YesNoFilter extends com.busit.App implements com.busit.Consumer
{
	public function consume($message, $in)
	{
		if( $in == "yes" ) // channel key
			...
		else if( $in == "no" )
			...
	}
}

?>
	

About automatic channels (cron)

Automatic channels are output channels of the producer pattern that are activated at given interval defined by the user. I.e.: "I want to get the GPS coordinate every minute".

Hence, the produce() method will be called at defined interval for that output channel only (even if other channels are connected).

About configurations

The configuration parameters as you designed them in the BusApp settings -and later completed by the user- are available in your code. You can thus retrieve them anywhere via the config() method that returns the value of the desired configuration parameter as a string.


$value = $this->config('my_parameter');

Bindable config parameters

Binding will occur before your BusApp is created, so the bindable config parameters will already be populated with the proper value.
Keep in mind that binding is optionnal from the user's perspective so you should always check if the config contains the value you expect !

Deep into message content

Busit offers an growing list of data structures that anyone can use or enhance. Thus, we strongly encourage all developers to acknowledge and use those for maximum compatibility between BusApps. Meanwhile, content types are not a constraint, they are not immutable and not restrictive... they are only a kind of very flexible indication on what sort of data is contained inside a message.

1) Principle

Lets define a (simplified, fake) known type that says that a "person" has a "name" and a "birthday".
So your IContent can be represented with this JSON:


{
	"id": 42,
	"name": "person",
	"data":
	{
		"name": null,
		"birthday": null
	}
}

$content = com.busit.Factory::content(42);
$content['name'] = 'Simon';
$content['birthday'] = 468021600;

2) Flexibility

As stated at the beginning, content types are just an indication on the intent of what maybe the message might contain, probably.

Noticed how much care was put in this sentence !?

This means that you can overload it and put more info inside it, ommit some that you do not have, or totally screw up everything:

"name" is now a number, why not,
"birthday", not any clue,
but hey, i've got his "favourite color" !


$content = com.busit.Factory::content(42);
$content['name'] = 123;
$content['favourite_color'] = 'blue';

The point is that you do not know what the user (or other BusApps) will send you, so you must check and anticipate to recieve anything ! However, in general, you can expect to have a minimum of trust, because other developers should offer proper content types when they can, and so should you. Nevertheless, with great flexibility comes great responsibility...


$content = $message->content();
if( $content->id() == 42 ) // because it could be any other content type
	if( $content['birthday'] != null && is_numeric($content['birthday']) ) // because it may be empty or crap
		$birthday = date('m/d/Y', $content['birthday']);

3) Compatibility

Sometimes, using the allowed flexibility that content types offer, it happens that you have more data available, and your message might be compatible with multiple other content types. For instance, you still want to expose a "person" using his "name" and "birthday", but you also have his location available. And, good news, there is a specific (fake again) known type for "geolocation" :


{
	"id": 666,
	"name": "geolocation",
	"data":
	{
		"latitude": null,
		"longitude": null
	}
}

So you can expose the two known types at once using the compatibility array:


{
	"id": 42,
	"name": "person",
	"compatibility": [666],
	"data":
	{
		"name": null,
		"birthday": null,
		"latitude": null,
		"longitude": null
	}
}

In your code, you can now check if a known type is compatible with another and behave accordingly:


$content = $message->content();
if( $content->compatible(666) )
{
	$content['latitude'] = 13.1281452;
	$content['longitude'] = -61.177160;
}

4) Format (nice display)

There are 2 different format templates that can be applied to the content when necessary: text and html. Depending on the needs, one may want to display the data (i.e.) in a file so there should be some kind of representation of that data.


{
	"textFormat": "Name: {{name}}, Birthday: {{birthday}}",
	"htmlFormat": "Name: </em>{{name}}<em>, Birthday: <em>{{birthday}}</em>",
}

file_put_contents("profile.html", $content->toHtml());

The content type format can be modified according to your needs. Therefore, if you overload the content type, you can modify its representation as well.


$content['favourite_color'] = 'blue';
$content->textFormat($content->textFormat() . ", Favourite Color: {{favourite_color}}");

Data properties can be enclosed in double curly braces {{property}} so they will be replaced when required.
If a matching data property cannot be found, the tag is ignored and removed.
In order to access nested properties, you can use the dot as separator. Array elements can be accessed with numeric 0-based index:


{
	"data":
	{
		"name": null,
		"address":
		{
			"street": null,
			"city": null
		},
		"colors": []
	}
	"textFormat": "Name: {{name}}, City: {{address.city}}, Favourite color: {{colors.0}}"
}

Powerful yet incomplete because there is no current way of defining a simple format that implies loops or conditions,... If you really want to have total control over the representation, then consider implementing it directly in your BusApp.

Note that for the html format, some tags are not allowed (stripped): script, object, embed,...

5) Obtain a content type

As shown before, you can request a content type by its id using the Factory. Then, you receive a pre-filled content type with all data properties set to null.

If you specify a wrong (unknown) id, then it will fallback on the default content type with id zero:


{
	"id": 0,
	"name": "Data",
	"compatibility": [],
	"data": { "data": null },
	"textFormat": "{{data}}",
	"htmlFormat": "<pre>{{data}}</pre>"
}

When sending a new message to Busit, you can send the json representation of the content type and it will be automatically interpreted. If it fails, it will create a default content type with the data property set to whatever you sent.

If you want to completely forge a content type and not use the default, you can use the Factory with a negative id (whichever). This will return a completely empty IContent (no id, no name, no format,...).

The message content() method will always return a valid content type. It will eventually fallback on the default if it cannot resolve it.


$message->content(null);
echo $message->$content()->id(); // will echo '0' (default content type)

Exceptions, Retry and Abort

There are 3 builtin error handling strategies that you can use in your code, each with a specific function.

Be careful to the side effects

When an exception is thrown or not caught, it will stop the current processing of the BusApp. This means that if there are multiple outputs that need to be pulled and one of them throws an exception, then the remaining output channels will not be processed. In such a case, it may be best to resort to the user notification system and return no message (null) instead.

1) Retry

When your code encounters an error that is recoverable with time and therefore can be simply retried later, then you should throw a Retry exception. This will silently stop the current processing and retry the operation according to the retry policy defined by the administrator.

Typical example : if a momentary loss of connection to a dependent API is detected and should normally come back up in a few minutes


if( date('Gi') > 2330 ) // retry if later than 23h30
	throw new com.busit.Retry();
	

2) Abort

When your code detects abnormalities from dependent resources or from the user configuration that would yield the same error over and over again, then you should throw an Abort exception. The message will be transmitted to the user to inform him about the issue. Then, the process will stop and be marked as a failure.

Typical example : if the user credentials to some service are invalid


if( $config == null )
	throw new com.busit.Abort("Please reconfigure your BusApp");
	

3) None

Any other unhandled exception will be interpreted by the system as a failure from you, the developer ! The message of the error will be notified to you such with a complete stack trace so that you can see what is going on. The process will stop and be marked as a failure.

Typical example: null pointer exceptions


$a = (2+2) / (2-2); // Exception : division by zero
	
Is something unclear, you've spotted a mistake, or you need more details ?

Then please help us improve this doc.
Or contact us directly.