|
| 1 | +Addding a New Action |
| 2 | +==================== |
| 3 | + |
| 4 | +Trigger actions can be added in your Cloe plugin in the ``enroll`` method: |
| 5 | + |
| 6 | +.. code-block:: cpp |
| 7 | +
|
| 8 | + class YourPlugin : public cloe::Controller { |
| 9 | + // ... |
| 10 | +
|
| 11 | + void enroll(cloe::Registrar& r) override { |
| 12 | + // This is where you add your calls to register various things with |
| 13 | + // the cloe engine, including actions. |
| 14 | + r.register_action<YourActionFactory>(); |
| 15 | + } |
| 16 | + } |
| 17 | +
|
| 18 | +And it looks like what ``register_action`` wants is a ``cloe::ActionFactoryPtr``, |
| 19 | +but why? Well, trigger actions are most often created in stackfiles, as |
| 20 | +something compatible with JSON. The ``ActionFactory`` takes a JSON object or |
| 21 | +a string, and makes an ``Action`` out of it. |
| 22 | + |
| 23 | +Defining an Action |
| 24 | +------------------ |
| 25 | + |
| 26 | +Let's look at a simple action defined in the runtime package in the file |
| 27 | +``cloe/trigger/example_actions.hpp``: ``Log``. |
| 28 | + |
| 29 | +We can see there is an include for ``<cloe/trigger.hpp>``. This file can also |
| 30 | +be found in the runtime package, and you should have a look at the |
| 31 | +documentation there for the ``Action`` interface. |
| 32 | + |
| 33 | +Then we come to the source for the ``Log`` action: |
| 34 | + |
| 35 | +.. code-block:: cpp |
| 36 | +
|
| 37 | + class Log : public Action { |
| 38 | + public: |
| 39 | + Log(const std::string& name, LogLevel level, const std::string& msg) |
| 40 | + : Action(name), level_(level), msg_(msg) {} |
| 41 | + ActionPtr clone() const override { return std::make_unique<Log>(name(), level_, msg_); } |
| 42 | + void operator()(const Sync&, TriggerRegistrar&) override { |
| 43 | + logger()->log(level_, msg_.c_str()); |
| 44 | + } |
| 45 | + bool is_significant() const override { return false; } |
| 46 | +
|
| 47 | + protected: |
| 48 | + void to_json(Json& j) const override { |
| 49 | + j = Json{ |
| 50 | + {"level", logger::to_string(level_)}, |
| 51 | + {"msg", msg_}, |
| 52 | + }; |
| 53 | + } |
| 54 | +
|
| 55 | + private: |
| 56 | + LogLevel level_; |
| 57 | + std::string msg_; |
| 58 | + }; |
| 59 | +
|
| 60 | +The constructor is only really relevant for the ``LogFactory``, so we'll ignore |
| 61 | +that for now. |
| 62 | + |
| 63 | +Then there is the ``clone()`` method. This should return another new instance |
| 64 | +of this action that can do the same thing again. This is important because the |
| 65 | +user can request that actions be repeated, which results in clones occurring. |
| 66 | + |
| 67 | +Now we come to the ``operator()`` method. This is where the real work of the |
| 68 | +action occurs. It takes two arguments, which you will probably never use. If |
| 69 | +you need them, you'll know. |
| 70 | +Inside the body of this function you can do what the action says it will do. |
| 71 | + |
| 72 | +For the ``is_significant()`` method, we tell Cloe whether this action can |
| 73 | +affect the simulation. If in doubt, answer yes. The ``Log`` action is one of |
| 74 | +the few actions that is not significant. |
| 75 | + |
| 76 | +Finally, we need to implement the ``to_json()`` method. This should provide the |
| 77 | +extra fields in the JSON representation that are needed to recreate this object |
| 78 | +with the ``LogFactory``. Note that there is no name of this action. That's |
| 79 | +because that is added automatically by Cloe, in order to prevent errors from |
| 80 | +slipping in. |
| 81 | + |
| 82 | +Defining the ActionFactory |
| 83 | +-------------------------- |
| 84 | + |
| 85 | +Defining the ``ActionFactory`` is often more work than the action itself. We |
| 86 | +need to think about how we want to allow the action to be created. |
| 87 | +At first glance, the implmentation looks simple: |
| 88 | + |
| 89 | +.. code-block:: cpp |
| 90 | +
|
| 91 | + class LogFactory : public ActionFactory { |
| 92 | + public: |
| 93 | + using ActionType = Log; |
| 94 | + LogFactory() : ActionFactory("log", "log a message with a severity") {} |
| 95 | + TriggerSchema schema() const override; |
| 96 | + ActionPtr make(const Conf& c) const override; |
| 97 | + ActionPtr make(const std::string& s) const override; |
| 98 | + }; |
| 99 | +
|
| 100 | +But then we see that the implementation for three methods is not inline. We'll |
| 101 | +have a look at each of these in turn, but first: the using statement. |
| 102 | +This statement defines what the output type of this action factory is, and is |
| 103 | +necessary for properly registering the action factory. |
| 104 | + |
| 105 | +The constructor of the action factory simply calls the super-constructor and |
| 106 | +supplies the *default* name and the description of the *action*. |
| 107 | + |
| 108 | +The ``schema()`` method is used to define the trigger schema, which among other |
| 109 | +things, lets Cloe validate input and also document the action. |
| 110 | + |
| 111 | +.. code-block:: cpp |
| 112 | +
|
| 113 | + TriggerSchema LogFactory::schema() const { |
| 114 | + return TriggerSchema{ |
| 115 | + this->name(), |
| 116 | + this->description(), |
| 117 | + InlineSchema("level and message to send", "[level:] msg", true), |
| 118 | + Schema{ |
| 119 | + {"level", make_prototype<std::string>("logging level to use")}, |
| 120 | + {"msg", make_prototype<std::string>("message to send").require()}, |
| 121 | + }, |
| 122 | + }; |
| 123 | + } |
| 124 | +
|
| 125 | +The ``make(const Conf&)`` method takes the object configuration, and reads the |
| 126 | +variables that are necessary for configuration. |
| 127 | + |
| 128 | +.. code-block:: cpp |
| 129 | +
|
| 130 | + ActionPtr LogFactory::make(const Conf& c) const { |
| 131 | + auto level = logger::into_level(c.get_or<std::string>("level", "info")); |
| 132 | + return std::make_unique<Log>(name(), level, c.get<std::string>("msg")); |
| 133 | + } |
| 134 | +
|
| 135 | +The ``make(const std::string&)`` method takes a string, and tries to parse this |
| 136 | +into something that it can fill into a Conf and pass on to the method above. |
| 137 | +This is often the most complex function, but it makes using triggers by |
| 138 | +hand much much easier. This just takes a string in the format: ``level: |
| 139 | +message`` and packs it into the JSON object structure the simpler ``make()`` |
| 140 | +method needs. |
| 141 | + |
| 142 | +.. code-block:: cpp |
| 143 | +
|
| 144 | + ActionPtr LogFactory::make(const std::string& s) const { |
| 145 | + auto level = spdlog::level::info; |
| 146 | + auto pos = s.find(":"); |
| 147 | + std::string msg; |
| 148 | + if (pos != std::string::npos) { |
| 149 | + try { |
| 150 | + level = logger::into_level(s.substr(0, pos)); |
| 151 | + if (s[++pos] == ' ') { |
| 152 | + ++pos; |
| 153 | + } |
| 154 | + msg = s.substr(pos); |
| 155 | + } catch (...) { |
| 156 | + msg = s; |
| 157 | + } |
| 158 | + } else { |
| 159 | + msg = s; |
| 160 | + } |
| 161 | +
|
| 162 | + auto c = Conf{Json{ |
| 163 | + {"level", logger::to_string(level)}, |
| 164 | + {"msg", msg}, |
| 165 | + }}; |
| 166 | + if (msg.size() == 0) { |
| 167 | + throw TriggerInvalid(c, "cannot log an empty message"); |
| 168 | + } |
| 169 | + return make(c); |
| 170 | + } |
0 commit comments