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.
There are 2 basic requirements for a BusApp:
define("__CLASSNAME__", "\\HelloWorld");
__CLASSNAME__ = "HelloWorld"
public class BusApp {
public static final String __CLASSNAME__ = "HelloWorld";
}
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:
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)
}
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.
This means that internal variables will be maintained between consume() and subsequent produce() or transform() calls.
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.
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;
}
<?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;
}
}
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.
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;
}
<?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) {}
}
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.
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;
}
<?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) {}
}
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.
Messages are composed of the following elements:
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();
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;
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:
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.
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" )
...
}
}
?>
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).
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');
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 !
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.
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;
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']);
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;
}
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,...
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)
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.
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();
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");
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