feat(data): raise your flag
Description
Imagine the following application configuration (in pseudo Sight APPXML):
<plugin>
<config>
<name="Foo"/>
<service uid="do_something_srv" type="sight::thing" />
<service uid="change_state_act" type="sight::module::ui::action" />
</config>
<config>
<name="Bar"/>
<service uid="do_stuff_srv" type="sight::stuff" />
</config>
</plugin>
Imagine that in the following application:
- one must be able to switch between the configs
Foo
andBar
at any time -
change_state_act
inFoo
changes the meaning of some data shared between the two configs (let's say, asight::data::image
which contents may describe something else based on that button).
Currently, there is no simple way to forward the state of change_state_act
to Bar
, unless the two configs are started
(which is not always possible in an application in which there is no sequencer
-like mechanic).
One possibility is to modify the service to add an extra inout
data in which some state can be forwarded based on an internal mechanic:
<!-- Foo config-->
<connect>
<signal>change_state_act/clicked</signal>
<slot>do_something_srv/write_something</slot>
</connect>
In which write_something
would write something in a data
passed to the service.
Another alternative is to work with fields, and inspect them C++-side again. This won't work if the concerned data is not common do configs Foo
and Bar
.
Both solutions introduce code bloat, and require C++ code changes, which isn't always convenient when working with subprojects.
Possible solution
I propose a new data
and a new service
driving it for (initialization/reset purposes only).
For now, let's name them sight::data::flags
and sight::service::reset_flags
.
data::flags
The goal of this data
is to provide an XML-compatible interface that wouldn't require any C++ modification (after implementation, of course) when states (flags) need to be forwarded to other configs/shared across the application. Such data
would only require to be declared at the top-level plugin.xml
and passed to all config
s needing to set them.
For how the set of flags can be set through XML while staying compatible with the <object/>
tag without modifying this code, see service::reset_flags
described below.
A data::flags
would be a (very) simple wrapper around std::map<std::string, bool>
. The idea is that it will generate signals and slots dynamically for each flag name.
The following snippet provides a mostly complete synopsis of the public API. NOTE: I'm writing this in a text editor without proper syntax check, please be cautious while reading it (don't take everything for correct).
sight::data::flags public API synospis
#include <functional>
#include <map>
#include <optional>
#include <ranges>
namespace sight::data
{
class flags : public has_slots, public has_signals
{
public:
flags(std::map<std::string, bool> _init = {}) noexcept;
flags(const flags&) = default;
flags(flags&&) noexcept = default;
flags& operator=(const flags&) = default;
flags& operator=(flags&&) noexcept = default;
[[nodiscard]] friend bool operator==(const flags&, const flags&) = default;
[[nodiscard]] friend bool operator!=(const flags&, const flags&) = default;
[[nodiscard]] friend auto operator<=>(const flags&, const flags&) = default;
//---------------------------------------------------------------------
struct signals
{
// Assume these are propers sight signal types
using changed_t = void(bool);
using overriden_t = void(bool);
using removed_t = void(void);
};
struct slots
{
using set_flag_t = std::function<void(std::string, bool)>; /*name, new value*/
using add_flag_t = std::function<void(std::string, bool)>; /*name, initial*/
};
//---------------------------------------------------------------------
// Modifiers
/// Adds a new flag if it didn't existed yet
void add(std::string _name, bool initial = false);
/// Adds a new flag, or overrides its value with the new one provided if it already exists
void add_or_override(std::string _name, bool initial = false);
/// Sets a flag, it it exists
void set(std::string _name, bool _value);
/// Removes an existing flag
void remove(std::string _name);
//---------------------------------------------------------------------
// Content inspection
/// Returns a view of the current flags, locked until the value stored expires.
[[nodiscard]]
auto keys() const noexcept;
/// Returns the value of a flag, or std::nullopt if it doesn't exist.
[[nodiscard]]
std::optional<bool> get(std::string _flag) const noexcept;
private:
std::map<std::string, bool> m_flags; // For exposition only
};
} // namespace sight::data
The following snippet provides a POOC implementation of sight::data::flags
. This has not been tested. Please use at your own risk (please consider each line carefully while implementing).
POOC implementation of sight::data::flags
#include <functional>
#include <map>
#include <optional>
#include <ranges>
namespace sight::data
{
class flags : public has_slots, public has_signals
{
public:
flags(std::map<std::string, bool> _init = {}) noexcept : m_flags{std::move(init)}
{}
flags(const flags&) = default;
flags(flags&&) noexcept = default;
flags& operator=(const flags&) = default;
flags& operator=(flags&&) noexcept = default;
[[nodiscard]] friend bool operator==(const flags&, const flags&) = default;
[[nodiscard]] friend bool operator!=(const flags&, const flags&) = default;
[[nodiscard]] friend auto operator<=>(const flags&, const flags&) = default;
//---------------------------------------------------------------------
struct signals
{
// Assume these are propers sight signal types
using changed_t = void(bool);
using overriden_t = void(bool);
using removed_t = void(void);
};
struct slots
{
using set_flag_t = std::function<void(std::string, bool)>; /*name, new value*/
using add_flag_t = std::function<void(std::string, bool)>; /*name, initial*/
};
//---------------------------------------------------------------------
// Modifiers
/// Adds a new flag if it didn't existed yet
void add(std::string _name, bool initial = false)
{
if(m_flags.contains(_name))
{
// SIGHT_ASSERT or SIGHT_ERROR or throw some exception
return;
}
add_or_override(std::move(_name), initial);
}
/// Adds a new flag, or overrides its value with the new one provided if it already exists
void add_or_override(std::string _name, bool initial = false)
{
std::scoped_lock<std::mutex> lock {m_mutex};
m_flags[_name] = initial;
const auto changed_signal_key = _name + "_changed"; // Called when the flag value changes
const auto overriden_signal_key = _name + "_overriden"; // Called when this function is called
const auto removed_signal_key = _name + "_removed"; // Called when the
const auto set_slot_key = "set_" + _name;
const auto set_true_slot_key = "set_" + _name + "_true";
const auto set_false_slot_key = "set_" + _name + "_false";
const auto remove_slot_key = "remove_" + _name;
auto set_slot = [this, changed_signal_key](bool _b)
{
std::scoped_lock<std::mutex> lock {m_mutex};
// The flag may have been removed
if(const auto it = m_flags.find(_name); it != m_flags.end())
{
it->second = _b;
}
signal<signals::changed_t>(changed_signal_key)->async_emit(_b);
};
auto set_true_slot = [this, set_slot]()
{
set_slot(true);
};
auto set_false_slot = [this, set_slot]()
{
set_slot(false);
};
auto remove_slot = [this, _name]
{
remove(_name);
};
// Don't add if it already exists
if(not m_signals.contains(changed_signal_key))
{
new_signal<signals::changed_t>(changed_signal_key);
new_signal<signals::overriden_t>(overriden_signal_key);
new_signal<signals::remove_t>(removed_signal_key);
}
if(not m_slots.contains(set_slot_key))
{
new_slot(set_slot_key, std::move(set_slot));
new_slot(set_true_slot_key, std::move(set_true_slot));
new_slot(set_false_slot_key, std::move(set_false_slot));
new_slot(remove_slot_key, std::move(remove_slot));
}
signal<signals::overriden_t>(overriden_signal_key)->async_emit(_initial);
}
/// Sets a flag, it it exists
void set(std::string _name, bool _value)
{
std::scoped_lock<std::mutex> lock {m_mutex};
if(const auto it = m_flags.find(_name); it != m_flags.end())
{
it->second = _value;
const auto changed_signal_key = _name + "_changed"
signal<signals::changed_t>(changed_signal_key)->async_emit(_b);
}
}
/// Removes an existing flag
void remove(std::string _name)
{
std::scoped_lock<std::mutex> lock {m_mutex};
if(const auto existed = m_flags.erase(_name); existed > 0)
{
signal<signals::remove_t>(_name + "_removed")->async_emit();
// Remove the signals from the map
// TODO: I'm not sure it's safe, we may keep them if it's not, they'll just not get triggered, with the implementation provided here, at least
}
}
//---------------------------------------------------------------------
// Content inspection
/// Returns a view of the current flags, locked until the value stored expires.
[[nodiscard]]
inline auto keys() const noexcept
{
// Maybe a custom class would be better to avoid giving access to the lock
return std::tie(std::views::keys(m_flags), std::scoped_lock<std::mutex> {m_mutex});
}
/// Returns the value of a flag, or std::nullopt if it doesn't exist.
[[nodiscard]]
inline std::optional<bool> get(std::string _flag) const noexcept
{
std::scoped_lock<std::mutex> lock {m_mutex};
if(const auto it = m_flags.find(_flag); it != m_flags.cend())
{
return it->second;
}
return std::nullopt;
}
private:
std::mutex m_mutex;
std::map<std::string, bool> m_flags;
};
} // namespace sight::data
service::reset_flags
The goal of this service
is to avoid having to introduce changes in how <object/>
XML tags are parsed, which would be a safety concern, considering how much we use this.
The idea is to pass a data::flags
as input (inout
or out
), and initialise upon starting
according to the configuration parsed upon configuring
. Optionally, it may provide slots to reset each individual flag to its initial value, but I won't show that here for the sake of simplicity. Note that this could be managed by the data itself, it just has to keep trace of the original values (not shown in the implementation above).
I won't provide a C++ synopsis, which sounds trivial at the time of writing.
XML API of service::reset_flags
<service uid="reset_flags_srv" type="sight::service::reset_flags">
<inout key="flags" uid="status_flags" />
<flags>
<flag id="stuff_checked" value="false"/>
<flag id="task_completed" value="false"/>
<flag id="enabled_cool_thing" value="true"/>
</flags>
</service>
<connect>
<signal>restart_task_action/triggered</signal>
<slot>reset_flags_srv/reset_task_completed</slot>
<!-- OR, according to the API above -->
<slot>status_flags/set_task_completed_false</slot>
<!-- But this requires to remember the initial value (though this shouldn't be *that* hard) -->
<!-- OR, if data::flags keeps trace of the original values -->
<slot>status_flags/reset_task_completed</slot>
</connect>
<!-- ... -->
<start uid="reset_flags_srv" />
Of course, we may very well choose to modify <object/>
parsing to introduce a new init_value
attribute. I don't think this is a good idea, though I don't have much to back this claim up.
Additional modifications
- We could modify the most-used services (
actions
,parameters
, etc.), so they take adata::flags
as input, an based on values specified in theconfig
tag,enable
ordisable
themselves uponstart
/update
, e.g.:
<service uid="action" type="sight::...::action">
<in key="status_flags" uid="app_status_flags"/>
<config
enable_requires_flag="some_service_started"
show_requires_flag="task_bar_completed"
/>
</service>
Then, in starting
and updating
:
Pseudo-code of modified ui::action to support data::flags
// hpp
class action
{
struct config
{
// This isn't stored here in the current implementation, but this is clearer for the needs of this issues
bool initially_shown = {};
bool initially_enabled = {};
// New members
std::optional<std::string> enable_flag = {};
std::optional<std::string> show_flag = {};
};
data::ptr<data::flags, data::acces::in> m_flags = {...};
};
// cpp
void action::starting()
{
bool enable = m_config.initially_enabled;
bool show = m_config.initially_shown;
if(const auto flags = m_flags.const_lock(); flags != nullptr)
{
// Was it specified in the config?
if(m_config.enable_flag.has_value())
{
// Is the flag set in the data::flags?
if(const auto enable_flag = flags->get(*m_config.enable_flag).has_value(); enable_flag.has_value())
{
enable = *enable_flag;
}
}
// ...
// (Do the same for the visibility)
}
set_enabled(enable);
set_visible(show);
}
void action::updating()
{
// Same as above, or whatever
}
Functional specifications
Workflow, UX/UI design, screenshots, etc...
Technical specifications
Details of the implementation.
Test plan
Unit tests, and if possible, test in a application before merging.