Webthing-CPP: a modern CPP implementation of the WebThings API


LCOV - code coverage report
Current view: top level - webthing - thing.hpp (source / functions) Coverage Total Hit
Test: filtered_coverage.info Lines: 94.5 % 183 173
Test Date: 2025-03-15 12:45:00 Functions: 100.0 % 38 38

            Line data    Source code
       1              : // Webthing-CPP
       2              : // SPDX-FileCopyrightText: 2023-present Benno Waldhauer
       3              : // SPDX-License-Identifier: MIT
       4              : 
       5              : #pragma once
       6              : 
       7              : #include <vector>
       8              : #include <bw/webthing/action.hpp>
       9              : #include <bw/webthing/constants.hpp>
      10              : #include <bw/webthing/event.hpp>
      11              : #include <bw/webthing/json.hpp>
      12              : #include <bw/webthing/property.hpp>
      13              : #include <bw/webthing/storage.hpp>
      14              : 
      15              : namespace bw::webthing {
      16              : 
      17              : class Thing
      18              : {
      19              : public:
      20              :     typedef std::function<std::shared_ptr<Action> (std::optional<json> input)> ActionSupplier;
      21              :     struct AvailableAction
      22              :     {
      23              :         json metadata;
      24              :         ActionSupplier class_supplier;
      25              :     };
      26              : 
      27              :     typedef std::function<void(const std::string& /*topic*/, const json& /*message*/)> MessageCallback; 
      28              : 
      29           36 :     Thing(std::string id, std::string title, std::vector<std::string> type, std::string description = "")
      30          108 :         : id(id), title(title), type(type), description(description)
      31              :     {
      32           36 :     }
      33              : 
      34            7 :     Thing(std::string id, std::string title, std::string type = "", std::string description = "")
      35           28 :         : Thing(id, title, std::vector<std::string>{type}, description)
      36              :     {
      37           14 :     }
      38              : 
      39           19 :     json as_thing_description() const
      40              :     {
      41              :         json thing({
      42           19 :             {"id", id},
      43           19 :             {"title", title},
      44           19 :             {"@context", context},
      45           19 :             {"@type", type},
      46           19 :             {"properties", get_property_descriptions() },
      47           19 :             {"actions", json::object()},
      48           19 :             {"events", json::object()},
      49           19 :             {"description", description},
      50              :             {"links", {
      51           19 :                 {{"rel", "properties"}, {"href", href_prefix + "/properties"}},
      52           19 :                 {{"rel", "actions"}, {"href", href_prefix + "/actions"}},
      53           19 :                 {{"rel", "events"}, {"href", href_prefix + "/events"}}
      54              :             }}
      55         1026 :         });
      56              : 
      57           19 :         if(ui_href)
      58              :         {
      59           11 :             thing["links"].push_back({
      60              :                 {"rel", "alternate"}, {"mediaType", "text/html"}, {"href", *ui_href}
      61              :             });
      62              :         }
      63              : 
      64           23 :         for(auto& aa : available_actions)
      65              :         {
      66            4 :             std::string name = aa.first;
      67            4 :             thing["actions"][name] = aa.second.metadata;
      68           44 :             thing["actions"][name]["links"] = {{{"rel", "action"}, {"href", href_prefix + "/actions/" + name}}}; 
      69            0 :         }
      70              : 
      71           23 :         for(auto& ae : available_events)
      72              :         {
      73            4 :             std::string name = ae.first;
      74            4 :             thing["events"][name] = ae.second;
      75           44 :             thing["events"][name]["links"] = {{{"rel", "event"}, {"href", href_prefix + "/events/" + name}}};
      76            0 :         }
      77           19 :         return thing;
      78         1084 :     }
      79              : 
      80           71 :     std::string get_href() const
      81              :     {
      82           71 :         if(href_prefix.size() > 0)
      83           38 :             return href_prefix;
      84           66 :         return "/";
      85              :     }
      86              : 
      87            2 :     std::optional<std::string> get_ui_href() const
      88              :     {
      89            2 :         return ui_href;
      90              :     }
      91              : 
      92            1 :     void set_ui_href(std::string href)
      93              :     {
      94            1 :         ui_href = href;
      95            1 :     }
      96              : 
      97           21 :     std::string get_id() const
      98              :     {
      99           21 :         return id;
     100              :     }
     101              : 
     102           11 :     std::string get_title() const
     103              :     {
     104           11 :         return title;
     105              :     }
     106              : 
     107            1 :     std::string get_description() const
     108              :     {
     109            1 :         return description;
     110              :     }
     111              : 
     112            2 :     std::vector<std::string> get_type() const
     113              :     {
     114            2 :         return type;
     115              :     }
     116              : 
     117            2 :     std::string get_context() const
     118              :     {
     119            2 :         return context;
     120              :     }
     121              : 
     122            1 :     void set_context(std::string context)
     123              :     {
     124            1 :         this->context = context;
     125            1 :     }
     126              : 
     127           19 :     json get_property_descriptions() const
     128              :     {
     129           19 :         auto pds = json::object();
     130           36 :         for(const auto& p : properties)
     131           17 :             pds[p.first] = p.second->as_property_description();
     132           19 :         return pds;
     133            0 :     }
     134              : 
     135              :     // Get the thing's actions as json array
     136              :     // action_name -- Optional action name to get description for
     137            8 :     json get_action_descriptions(std::optional<std::string> action_name = std::nullopt) const
     138              :     {
     139            8 :         json descriptions = json::array();
     140              : 
     141           21 :         for(const auto& action_entry : actions)
     142           20 :             for(const auto& action : action_entry.second)
     143            7 :                 if(!action_name || action_name ==  action_entry.first)
     144            7 :                     descriptions.push_back(action->as_action_description());
     145              : 
     146            8 :         return descriptions;
     147            0 :     }
     148              : 
     149              :     // Get the thing's events as json array.
     150              :     // event_name -- Optional event name to get description for
     151           15 :     json get_event_descriptions(const std::optional<std::string>& event_name = std::nullopt) const
     152              :     {
     153           15 :         json descriptions = json::array();
     154              : 
     155           55 :         for(const auto& evt : events)
     156           40 :             if(!event_name || event_name == evt->get_name())
     157           23 :                 descriptions.push_back(evt->as_event_description());
     158              : 
     159           15 :         return descriptions;
     160            0 :     }
     161              : 
     162           17 :     void add_property(std::shared_ptr<PropertyBase> property)
     163              :     {
     164           17 :         property->set_href_prefix(href_prefix);
     165           17 :         properties[property->get_name()] = property;
     166           17 :     }
     167              : 
     168              :     void remove_property(const PropertyBase& property)
     169              :     {
     170              :         properties.erase(property.get_name());
     171              :     }
     172              : 
     173              :     // Find a property by name
     174           35 :     std::shared_ptr<PropertyBase> find_property(std::string property_name) const
     175              :     {
     176           35 :         if(properties.count(property_name) > 0)
     177           32 :             return properties.at(property_name);
     178            3 :         return nullptr;
     179              :     }
     180              : 
     181              :     template<class T>
     182           17 :     void set_property(std::string property_name, T value)
     183              :     {
     184           17 :         auto prop = find_property(property_name);
     185           17 :         if(prop)
     186           18 :             prop->set_value(value);
     187           17 :     }
     188              : 
     189              :     template<class T>
     190            5 :     std::optional<T> get_property(std::string property_name) const
     191              :     {
     192            5 :         auto property = find_property(property_name);
     193            5 :         if(property)
     194            5 :             return property->get_value<T>();
     195            0 :         return std::nullopt;
     196            5 :     }
     197              : 
     198              :     // Get a mapping of all properties and their values.
     199            4 :     json get_properties() const
     200              :     {
     201            4 :         auto json = json::object();
     202              : 
     203           11 :         for(const auto& pe : properties)
     204              :         {
     205            7 :             json[pe.first] = pe.second->get_property_value_object()[pe.first];
     206              :         }
     207            4 :         return json;
     208            0 :     }
     209              : 
     210              :     //Determine whether or not this thing has a given property.
     211              :     // property_name -- the property to look for
     212              :     bool has_property(std::string property_name) const
     213              :     {
     214              :        return properties.find(property_name) != properties.end();
     215              :     }
     216              : 
     217           17 :     void property_notify(json property_status_message)
     218              :     {
     219           17 :         logger::debug("thing::property_notify : " + property_status_message.dump());
     220           30 :         for(auto& observer : observers)
     221           13 :             observer( id + "/properties", property_status_message);
     222           17 :     }
     223              : 
     224              :     // Perform an action on the thing.
     225              :     // name -- name of the action
     226              :     // input -- any action inputs 
     227           28 :     std::shared_ptr<Action> perform_action(std::string name, std::optional<json> input = std::nullopt)
     228              :     {
     229           28 :         if(available_actions.count(name) == 0)
     230            1 :             return nullptr;
     231              : 
     232           27 :         auto& action_type = available_actions[name];
     233              : 
     234           27 :         if(action_type.metadata.contains("input"))
     235              :         {
     236              :             try
     237              :             {
     238           37 :                 validate_value_by_scheme(input.value_or(json()), action_type.metadata["input"]);
     239              :             }
     240            8 :             catch(std::exception& ex)
     241              :             {
     242           24 :                 logger::debug("action: '" + name + "' invalid input: " + 
     243           32 :                     input.value_or(json()).dump() +" error: " + ex.what());
     244            8 :                 return nullptr;
     245            8 :             }
     246              :         }
     247              : 
     248              :         try
     249              :         {
     250           21 :             auto action = action_type.class_supplier( std::move(input) );
     251           17 :             action->set_href_prefix(href_prefix);
     252           17 :             action_notify(action_status_message(action));
     253           17 :             actions[name].add(action);
     254           17 :             return action;
     255           17 :         }
     256            2 :         catch(std::exception& ex)
     257              :         {
     258            2 :             logger::debug("Construction of action '" + name + "' failed with error: " + ex.what());
     259            2 :             return nullptr;
     260            2 :         }
     261              :     }
     262              : 
     263              :     // Add an available action.
     264              :     // name -- name of the action
     265              :     // metadata -- action metadata, i.e. type, description, etc. as a json object
     266              :     // class_supplier -- function to instantiate this action
     267           16 :     void add_available_action(std::string name, json metadata, ActionSupplier class_supplier)
     268              :     {
     269           16 :         if(!metadata.is_object())
     270            3 :             throw ActionError("Action metadata must be encoded as json object.");
     271              : 
     272           15 :         available_actions[name] = { metadata, class_supplier };
     273           15 :         actions[name] = {action_storage_config};
     274           30 :     }
     275              : 
     276           33 :     void action_notify(json action_status_message)
     277              :     {
     278           33 :         logger::debug("thing::action_notify : " + action_status_message.dump());
     279           42 :         for(auto& observer : observers)
     280            9 :             observer( id + "/actions", action_status_message);
     281           33 :     }
     282              : 
     283              :     // Get an action by its name and id
     284              :     // return the action when found, std::nullopt otherwise
     285           11 :     std::shared_ptr<Action> get_action(std::string action_name, std::string action_id) const
     286              :     {
     287           11 :         if(actions.count(action_name) == 0)
     288            2 :             return nullptr;
     289              : 
     290            9 :         const auto& actions_for_name = actions.at(action_name);
     291           13 :         for(const auto& action : actions_for_name)
     292           10 :             if(action->get_id() == action_id)
     293            6 :                 return action;
     294              : 
     295            3 :         return nullptr;
     296              :     }
     297              : 
     298              :     // Remove an existing action identified by its name and id
     299              :     // Returns bool indicating the presence of the action 
     300            2 :     bool remove_action(std::string action_name, std::string action_id)
     301              :     {
     302            2 :         auto action = get_action(action_name, action_id);
     303            2 :         if(!action)
     304            0 :             return false;
     305              :         
     306            2 :         action->cancel();
     307            2 :         auto& as = actions[action_name];
     308            6 :         as.remove_if([&action_id](auto a){return a->get_id() == action_id;});
     309            2 :         return true;
     310            2 :     }
     311              : 
     312              :     // Add a new event and notify subscribers
     313           14 :      void add_event(std::shared_ptr<Event> event)
     314              :      {
     315           14 :         events.add(event);
     316           14 :         event_notify(*event);
     317           14 :      }
     318              : 
     319              :     // Add an available event.
     320              :     // name -- name of the event
     321              :     // metadata -- event metadata, i.e. type, description, etc., as a json object
     322           11 :     void add_available_event(std::string name, json metadata = json::object())
     323              :     {
     324           11 :         if(!metadata.is_object())
     325            3 :             throw EventError("Event metadata must be encoded as json object.");
     326              : 
     327           10 :         available_events[name] = metadata;
     328           10 :     }
     329              : 
     330           14 :     void event_notify(const Event& event)
     331              :     {
     332           14 :         if(available_events.count(event.get_name()) == 0)
     333            0 :             return;
     334              : 
     335           14 :         json message = event_message(event);
     336           14 :         logger::debug("thing::event_notify : " + message.dump());
     337              : 
     338           21 :         for(auto& observer : observers)
     339            7 :             observer( id + "/events/" + event.get_name(), message);
     340           14 :     }
     341              : 
     342              :     // Set the prefix of any hrefs associated with this thing.
     343           17 :     void set_href_prefix(std::string prefix)
     344              :     {
     345           17 :         href_prefix = prefix;
     346              : 
     347           29 :         for(const auto& property : properties )
     348           12 :             property.second->set_href_prefix(prefix);
     349              : 
     350           20 :         for(auto& action_entry : actions)
     351            3 :             for(auto& action : action_entry.second)
     352            0 :                 action->set_href_prefix(prefix);
     353           17 :     }
     354              : 
     355           17 :     void add_message_observer(MessageCallback observer)
     356              :     {
     357           17 :         observers.push_back(observer);
     358           17 :     }
     359              : 
     360              :     // configures the storage of events, should be set in initialization phase
     361              :     void configure_event_storage(const StorageConfig& config)
     362              :     {
     363              :         event_storage_config = config;
     364              :         events = {event_storage_config};
     365              :     }
     366              : 
     367              :     // configures the storage of actions, should be set in initialization phase
     368              :     // before actions are linked to the thing
     369              :     void configure_action_storage(const StorageConfig& config)
     370              :     {
     371              :         action_storage_config = config;
     372              :         for (auto& [action_name, actions] : actions)
     373              :         {
     374              :             actions = {action_storage_config};
     375              :         }
     376              :     }
     377              : 
     378              : protected:
     379              :     std::string id;
     380              :     std::string context = WEBTHINGS_IO_CONTEXT;
     381              :     std::string title;
     382              :     std::vector<std::string> type;
     383              :     std::string description;
     384              :     std::map<std::string, std::shared_ptr<PropertyBase>> properties;
     385              :     std::map<std::string, AvailableAction> available_actions;
     386              :     std::map<std::string, json> available_events;
     387              :     StorageConfig action_storage_config = {10000};
     388              :     std::map<std::string, FlexibleRingBuffer<std::shared_ptr<Action>>> actions;
     389              :     StorageConfig event_storage_config = {100000};
     390              :     SimpleRingBuffer<std::shared_ptr<Event>> events = {event_storage_config};
     391              :     std::string href_prefix;
     392              :     std::optional<std::string> ui_href;
     393              :     std::vector<MessageCallback> observers;
     394              : };
     395              : 
     396              : } // bw::webthing
        

Generated by: LCOV version 2.0-1