Copyright 2023 Travis J. West, https://traviswest.ca, Input Devices and Music Interaction Laboratory (IDMIL), Centre for Interdisciplinary Research in Music Media and Technology (CIRMMT), McGill University, Montréal, Canada, and Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRIStAL, F-59000 Lille, France
SPDX-License-Identifier: MIT
This binding stores the value of session data endpoints in JSON format using the RapidJSON library. A platform-specific plugin parameter is used to store and retrieve this data across sessions, allowing session data to be restored when loading a new session, such as after rebooting an embedded device.
Overview
The functionality of this session storage component is provided by its initialization and main subroutines. The main subroutine tracks changes to session data endpoints and saves updates by serializing JSON data into persistent storage using the platform-specific plugin parameter part. The initialization subroutine retrieves and deserializes the JSON data and restores the last recorded state of the session data. This component should be initialized before any components with session data, so that the restored state can be used by those components during their initialization subroutines.
This component is intended to be used as a part in a platform-specific storage component that provides RapidJSON compatible IStream and OStream parameters, and manages their initialization and lifetimes. In particular, the IStream should be initialized before calling this component's init subroutine, and can be cleaned up immediately afterwards. The OStream may be instantiated before each call to main, or kept around persistently, whichever is better suited for the platform.
A Word About Spelling
The upstream authors use the spelling rapidjson for the namespace and directory in which their library is located, which contradicts with our convention of seperating words by an underscore as in sygbp-rapid_json. Just so you know...
Implementation
Test Component
We define a component with some session data for testing.
This kind of wrapper is required so that side-effects and resources required to instantiate an output stream can be only invoked when necessary.
We will use the input and output string streams provided by RapidJSON for testing, so we define a type alias to avoid having to repeat this in each test case.
// @+'tests'
using TestStorage = RapidJsonSessionStorage<rapidjson::StringStream, OStream, decltype(test_component)>;
// @/
Accessing JSON Member Values
When initializing, persistent data endpoints must be set to the state contained in the JSON object's member fields. When updating in the external destinations subroutine, the value of persistent endpoints must be compared with the value of the JSON object's member fields. The following subroutine abstracts access to the JSON object's members, accepting a lambda that is used to perform the necessary specific functionality, so that the type safety checks needn't be repeated every time the JSON member data is accessed.
f(m, [](auto& arr, auto idx) { return arr[idx].GetDouble(); });
} elseifconstexpr (string_like<element_t<T>>)
{
if (m[0].IsString())
f(m, [](auto& arr, auto idx) { return arr[idx].GetString(); });
}
}
}
}
// @/
Init
A template-parameter input stream is used at the beginning of the initialization subroutine to load JSON data from storage.
// @+'init'
json.ParseStream(istream);
// @/
In case there was no data stored, e.g. if this is the first time ever booting the device or its flash memory was recently erased, then we set the json document to an empty object and return; there's nothing else to do.
// @+'init'
if (not json.IsObject())
{
json.SetObject();
return;
}
// @/
// @+'tests'
TEST_CASE("sygaldry RapidJSON creates object given empty input stream")
{
string ibuffer{""};
rapidjson::StringStream istream{ibuffer.c_str()};
TestStorage storage{};
storage.init(istream, test_component);
CHECK(storage.json.IsObject());
REQUIRE(storage.json.ObjectEmpty());
}
// @/
Otherwise, we iterate over each session data endpoint and attempt to set the value of the endpoint based on that stored in the JSON document. As seen above, this will silently fail if the stored value does not exist (e.g. if the session data endpoint was just added), or is the wrong type (e.g. if the session data endpoint was just edited). It remains as future work to attempt to coerce a stored value to the correct type in the latter case.
The structure of the external destinations subroutine actually mirrors that of the initialization subroutine, with one branch for the case where the JSON document doesn't have any existing data for an endpoint, and one where it does.
if (not json.HasMember(osc_path_v<T, Components>))
{
@{external_destinations not HasMember branch}
}
else
{
@{external_destinations HasMember branch}
}
}
});
// @/
In case the document doesn't already have a member for a given endpoint, one is added appropriately depending on the value type of that endpoint. String-like data in particular requires special handling, since RapidJSON needs to copy this data on the heap. This branch always results in a change to the JSON document, which is signaled via the updated boolean flag.
If a member already exists, then we check if its value has changed. For OccasionalValue types, this is a simple matter of checking the boolean interpretation of the endpoint. Otherwise, the current value of the JSON document member is compared with the value of the endpoint.
If either of the above branches results in a change to the document on any endpoint, then the document is sent to the template-parameter output stream for long-term storage.