GetCallResult browser bridge function implemented

JSBridgeArgumentWrap added to support method-defined arguments
JSBaseTransporter updated
This commit is contained in:
Ralph Wessel
2024-08-17 00:21:27 +01:00
parent 9c6680399c
commit 1624828724
13 changed files with 497 additions and 289 deletions
@@ -2,6 +2,7 @@
#include "Active/Setting/ValueSetting.h"
#include "Active/Setting/Values/StringValue.h"
#include "Speckle/Interface/Browser/JSPortal.h"
#include "Speckle/Interface/Browser/Bridge/Functions/GetBindingsMethodNames.h"
#include "Speckle/Interface/Browser/Bridge/Functions/RunMethod.h"
@@ -17,7 +18,7 @@ using namespace speckle::utility;
namespace speckle::interface::browser::bridge {
class BrowserBridge::ResultCache : public std::map<String, std::unique_ptr<active::serialise::Cargo>> {
class BrowserBridge::ResultCache : public std::map<String, std::unique_ptr<Cargo>> {
public:
//Mutex to control access to the cache
std::mutex mutex;
@@ -31,7 +32,7 @@ namespace speckle::interface::browser::bridge {
name: The JS object name
toReserve: The number of supported methods to reserve space for
--------------------------------------------------------------------*/
BrowserBridge::BrowserBridge(const speckle::utility::String& name) : JSObject{name} {
BrowserBridge::BrowserBridge(const String& name) : JSObject{name} {
//Populate the required browser bridge functions callable from JS
emplace_back(std::make_unique<GetBindingsMethodNames>(*this));
emplace_back(std::make_unique<RunMethod>(*this));
@@ -56,7 +57,7 @@ ValueSetting BrowserBridge::getMethodNames() const {
return: A pointer to the requested method (owner does not take ownership, nullptr = failure)
--------------------------------------------------------------------*/
Functional<>* BrowserBridge::getMethod(const speckle::utility::String& name) const {
Functional<>* BrowserBridge::getMethod(const String& name) const {
Functional<>* result = nullptr;
if (auto method = std::find_if(m_methods->begin(), m_methods->end(), [&](const auto& i) { return i->getName() == name; }); method != m_methods->end())
result = method->get();
@@ -70,7 +71,27 @@ Functional<>* BrowserBridge::getMethod(const speckle::utility::String& name) con
result: The result cargo to send back to the JS
requestID: The resquest ID from the JS caller (to correctly pair up the caller and result)
--------------------------------------------------------------------*/
void BrowserBridge::cacheResult(std::unique_ptr<active::serialise::Cargo> result, speckle::utility::String requestID) {
void BrowserBridge::cacheResult(std::unique_ptr<Cargo> result, String requestID) {
if (m_portal == nullptr)
throw; //TODO: Add exception detail
const std::lock_guard<std::mutex> lock(m_result->mutex);
(*m_result)[requestID] = std::move(result);
m_portal->execute(getName() + ".responseReady('" + requestID + "')"); //TODO: Need to confirm target object name
} //BrowserBridge::cacheResult
/*--------------------------------------------------------------------
Release the results linked to a specified request ID
requestID: The required result ID
return: The results linked to the specified ID (nullptr on failure)
--------------------------------------------------------------------*/
std::unique_ptr<Cargo> BrowserBridge::releaseResult(String requestID) {
std::unique_ptr<Cargo> result;
if (auto iter = m_result->find(requestID); iter != m_result->end()) {
result = std::move(iter->second);
m_result->erase(iter);
}
return result;
} //BrowserBridge::releaseResult
@@ -53,6 +53,12 @@ namespace speckle::interface::browser::bridge {
@param requestID The resquest ID from the JS caller (to correctly pair up the caller and result)
*/
void cacheResult(std::unique_ptr<active::serialise::Cargo> result, speckle::utility::String requestID);
/*!
Release the results linked to a specified request ID
@param requestID The required result ID
@return The results linked to the specified ID (nullptr on failure)
*/
std::unique_ptr<active::serialise::Cargo> releaseResult(speckle::utility::String requestID);
protected:
/*!
@@ -0,0 +1,35 @@
#include "Speckle/Interface/Browser/Bridge/Functions/GetCallResult.h"
#include "Speckle/Interface/Browser/Bridge/BrowserBridge.h"
#ifdef ARCHICAD
#include <ACAPinc.h>
#include <MessageLoopExecutor.hpp>
#endif
using namespace active::serialise;
using namespace speckle::serialise::jsbase;
using namespace speckle::interface::browser;
using namespace speckle::interface::browser::bridge;
/*--------------------------------------------------------------------
Constructor
bridge: The parent bridge object (provides access to bridge methods)
--------------------------------------------------------------------*/
GetCallResult::GetCallResult(BrowserBridge& bridge) : m_bridge{bridge},
JSFunction{"GetCallResult", [&](auto args) { return runMethod(args); }} {
} //GetCallResult::GetCallResult
/*--------------------------------------------------------------------
Run a specified bridge method
arguments: The method arguments
--------------------------------------------------------------------*/
std::unique_ptr<Cargo> GetCallResult::runMethod(JSBridgeArgumentWrap& argument) const {
//Confirm argument and function validity
if (!argument || (argument.getObjectName() != m_bridge.getName()))
return nullptr;
return m_bridge.releaseResult(argument.getRequestID());
} //GetCallResult::runMethod
@@ -0,0 +1,55 @@
#ifndef SPECKLE_INTERFACE_BRIDGE_GET_CALL_RESULT
#define SPECKLE_INTERFACE_BRIDGE_GET_CALL_RESULT
#include "Speckle/Interface/Browser/PlatformBinding.h"
#include "Speckle/Interface/Browser/JSFunction.h"
#include "Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.h"
namespace speckle::interface::browser::bridge {
class BrowserBridge;
/*!
Function to retrieve the names of the methods supported by the bridge
*/
class GetCallResult : public JSFunction<JSBridgeArgumentWrap, active::serialise::Cargo, PlatformBinding> {
public:
// MARK: - Constructors
/*!
Constructor
@param bridge The parent bridge object (provides access to bridge methods)
*/
GetCallResult(BrowserBridge& bridge);
/*!
Copy constructor
@param source The object to copy
*/
GetCallResult(const GetCallResult& source) = default;
/*!
Object cloning
@return A clone of this object
*/
GetCallResult* clonePtr() const override { return new GetCallResult{*this}; }
/*!
Get an argument instance for the function (used to deserialise/unpack incoming arguments)
@return An argument instance
*/
std::unique_ptr<active::serialise::Cargo> getArgument() const override;
private:
/*!
Run a specified bridge method
@param argument The method arguments
*/
std::unique_ptr<active::serialise::Cargo> runMethod(JSBridgeArgumentWrap& argument) const;
///The parent browser bridge
BrowserBridge& m_bridge;
};
}
#endif //SPECKLE_INTERFACE_BRIDGE_GET_CALL_RESULT
@@ -1,85 +0,0 @@
#include "Speckle/Interface/Browser/Bridge/JSBridgeArgument.h"
#include "Active/Serialise/Inventory/Inventory.h"
#include "Active/Serialise/Item/Wrapper/ValueWrap.h"
using namespace active::serialise;
using namespace speckle::interface::browser::bridge;
using namespace speckle::utility;
namespace {
using enum active::serialise::Entry::Type;
///The indices of the package items
enum FieldIndex {
objectName,
methodName,
requestID,
};
///The package inventory
auto myInventory = Inventory {
{
{ {"binding_name"}, objectName, attribute },
{ {"name"}, methodName, attribute },
{ {"request_id"}, requestID, attribute },
},
}.withType(&typeid(JSBridgeArgument));;
}
/*--------------------------------------------------------------------
Fill an inventory with the cargo items
inventory: The inventory to receive the cargo items
return: True if items have been added to the inventory
--------------------------------------------------------------------*/
bool JSBridgeArgument::fillInventory(active::serialise::Inventory& inventory) const {
inventory.merge(myInventory);
return true;
} //JSBridgeArgument::fillInventory
/*--------------------------------------------------------------------
Get the specified cargo
item: The inventory item to retrieve
return: The requested cargo (nullptr on failure)
--------------------------------------------------------------------*/
Cargo::Unique JSBridgeArgument::getCargo(const active::serialise::Inventory::Item& item) const {
if (item.ownerType != &typeid(JSBridgeArgument))
return nullptr;
switch (item.index) {
case FieldIndex::objectName:
return std::make_unique<ValueWrap<String>>(m_objectName);
case FieldIndex::methodName:
return std::make_unique<ValueWrap<String>>(m_methodName);
case FieldIndex::requestID:
return std::make_unique<ValueWrap<String>>(m_requestID);
default:
return nullptr; //Requested an unknown index
}
} //JSBridgeArgument::getCargo
/*--------------------------------------------------------------------
Set to the default package content
--------------------------------------------------------------------*/
void JSBridgeArgument::setDefault() {
m_objectName.clear();
m_methodName.clear();
m_requestID.clear();
} //JSBridgeArgument::setDefault
/*--------------------------------------------------------------------
Validate the cargo data
return: True if the data has been validated
--------------------------------------------------------------------*/
bool JSBridgeArgument::validate() {
return !m_objectName.empty() && !m_methodName.empty() && !m_requestID.empty();
} //JSBridgeArgument::validate
@@ -8,6 +8,11 @@ namespace speckle::interface::browser::bridge {
/*!
Base class for the argments passed from JavaScript to a named C++ method in a Speckle bridging object
NB: The JSBridgeArgumentWrap class will:
- Deserialise the essential attributes for determining the target method and arguments;
- Create the correct JSBridgeArgument subclass for the emthod/argument and populate it with the collected attributes
Therefore, there is no need for this class to handle any deserialisation, and subclasses should only handle the core arguments data
*/
class JSBridgeArgument : public active::serialise::Package {
public:
@@ -44,30 +49,25 @@ namespace speckle::interface::browser::bridge {
@return The request ID
*/
const speckle::utility::String& getRequestID() const { return m_requestID; }
/*!
Fill an inventory with the cargo items
@param inventory The inventory to receive the cargo items
@return True if items have been added to the inventory
*/
bool fillInventory(active::serialise::Inventory& inventory) const override { return false; } //Nothing to serialise at this level
/*!
Get the specified cargo
@param item The inventory item to retrieve
@return The requested cargo (nullptr on failure
*/
Cargo::Unique getCargo(const active::serialise::Inventory::Item& item) const override { return nullptr; } //Nothing to serialise at this level
// MARK: - Functions (serialisation)
// MARK: - Functions (mutating)
/*!
Fill an inventory with the cargo items
@param inventory The inventory to receive the cargo items
@return True if items have been added to the inventory
*/
bool fillInventory(active::serialise::Inventory& inventory) const override;
/*!
Get the specified cargo
@param item The inventory item to retrieve
@return The requested cargo (nullptr on failure)
*/
Cargo::Unique getCargo(const active::serialise::Inventory::Item& item) const override;
/*!
Set to the default package content
*/
Set to the default package content
*/
void setDefault() override;
/*!
Validate the cargo data
@return True if the data has been validated
*/
bool validate() override;
private:
///The name of the JS object the argument is targeting
@@ -0,0 +1,107 @@
#include "Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.h"
#include "Active/Serialise/Inventory/Inventory.h"
#include "Active/Serialise/Item/Wrapper/ValueWrap.h"
using namespace active::serialise;
using namespace speckle::interface::browser::bridge;
using namespace speckle::utility;
namespace {
using enum active::serialise::Entry::Type;
///The indices of the package items
enum FieldIndex {
objectName,
methodName,
requestID,
};
///The package inventory
auto myInventory = Inventory {
{
{ {"binding_name"}, objectName, attribute },
{ {"name"}, methodName, attribute },
{ {"request_id"}, requestID, attribute },
},
}.withType(&typeid(JSBridgeArgumentWrap));;
}
/*--------------------------------------------------------------------
Fill an inventory with the cargo items
inventory: The inventory to receive the cargo items
return: True if items have been added to the inventory
--------------------------------------------------------------------*/
bool JSBridgeArgumentWrap::fillInventory(active::serialise::Inventory& inventory) const {
if (!m_isReadingAttributes.has_value() || *m_isReadingAttributes)
inventory.merge(myInventory);
if (m_argument)
m_argument->fillInventory(inventory);
return true;
} //JSBridgeArgumentWrap::fillInventory
/*--------------------------------------------------------------------
Get the specified cargo
item: The inventory item to retrieve
return: The requested cargo (nullptr on failure)
--------------------------------------------------------------------*/
Cargo::Unique JSBridgeArgumentWrap::getCargo(const active::serialise::Inventory::Item& item) const {
if (item.ownerType != &typeid(JSBridgeArgumentWrap))
return nullptr;
switch (item.index) {
case FieldIndex::objectName:
return std::make_unique<ValueWrap<String>>(m_objectName);
case FieldIndex::methodName:
return std::make_unique<ValueWrap<String>>(m_methodName);
case FieldIndex::requestID:
return std::make_unique<ValueWrap<String>>(m_requestID);
default:
return nullptr; //Requested an unknown index
}
} //JSBridgeArgumentWrap::getCargo
/*--------------------------------------------------------------------
Set to the default package content
--------------------------------------------------------------------*/
void JSBridgeArgumentWrap::setDefault() {
m_objectName.clear();
m_methodName.clear();
m_requestID.clear();
m_argument.reset(); //This will be populated once the target bridge and method are known (and hence the required argument type)
} //JSBridgeArgumentWrap::setDefault
/*--------------------------------------------------------------------
Validate the cargo data
return: True if the data has been validated
--------------------------------------------------------------------*/
bool JSBridgeArgumentWrap::validate() {
return !m_objectName.empty() && !m_methodName.empty() && !m_requestID.empty() && (!m_argument | m_argument->validate());
} //JSBridgeArgumentWrap::validate
/*--------------------------------------------------------------------
Finalise the package attributes (called when isAttributeFirst = true and attributes have been imported)
return: True if the attributes have been successfully finalised (returning false will cause an exception to be thrown)
--------------------------------------------------------------------*/
bool JSBridgeArgumentWrap::finaliseAttributes() {
if (!m_isReadingAttributes.has_value() || !*m_isReadingAttributes ||m_objectName.empty() || m_methodName.empty())
return false;
m_isReadingAttributes = false;
//Use the deserialised target bridge and method to establish the required arguments (if any)
m_argument.reset(JSBridgeArgumentWrap::makeArgument(m_objectName, m_methodName));
//If the function doesn't take an argument, we still need to pass along the base class with object name, method etc
if (!m_argument)
m_argument = std::make_unique<JSBridgeArgument>(m_objectName, m_methodName, m_requestID);
return true;
} //JSBridgeArgumentWrap::finaliseAttributes
@@ -4,9 +4,24 @@
#include "Active/Serialise/Package/Package.h"
#include "Speckle/Interface/Browser/Bridge/JSBridgeArgument.h"
#include <unordered_map>
namespace speckle::interface::browser::bridge {
class JSBridgeArgument;
/*!
Factory function to make an argument object
@return A new argument object
*/
template<typename T>
void* constructArgument() {
try {
return new T();
} catch(...) {
return nullptr; //Object constructors should throw an exception if incoming data isn't viable (NB: only use for unrecoverable problems)
}
}
/*!
Wrapper for bridge function arguments, determing the target requirement on demand
@@ -19,7 +34,7 @@ namespace speckle::interface::browser::bridge {
/*!
Default constructor
*/
JSBridgeArgumentWrap();
JSBridgeArgumentWrap() {}
/*!
Copy constructor
*/
@@ -63,30 +78,85 @@ namespace speckle::interface::browser::bridge {
// MARK: - Functions (serialisation)
/*!
Fill an inventory with the cargo items
@param inventory The inventory to receive the cargo items
@return True if items have been added to the inventory
Determine if the package requires attributes to be imported first (primarily for unordered serialisation, e.g. JSON)
@return True if the package requires attributes first
*/
bool isAttributeFirst() const override { return m_isReadingAttributes.value_or(false); }
/*!
Fill an inventory with the cargo items
@param inventory The inventory to receive the cargo items
@return True if items have been added to the inventory
*/
bool fillInventory(active::serialise::Inventory& inventory) const override;
/*!
Get the specified cargo
@param item The inventory item to retrieve
@return The requested cargo (nullptr on failure)
*/
Get the specified cargo
@param item The inventory item to retrieve
@return The requested cargo (nullptr on failure)
*/
Cargo::Unique getCargo(const active::serialise::Inventory::Item& item) const override;
/*!
Set to the default package content
*/
Set to the default package content
*/
void setDefault() override;
/*!
Validate the cargo data
@return True if the data has been validated
*/
Validate the cargo data
@return True if the data has been validated
*/
bool validate() override;
/*!
Finalise the package attributes (called when isAttributeFirst = true and attributes have been imported)
@return True if the attributes have been successfully finalised (returning false will cause an exception to be thrown)
*/
bool finaliseAttributes() override;
/*!
Make an argument object for a specified bridge method
@param bridge The name of the target bridge
@param method The name of the target method
@return An argument object (nullptr on failure)
*/
static JSBridgeArgument* makeArgument(const speckle::utility::String& bridge, const speckle::utility::String& method) {
if (auto maker = m_argumentFactory.find(JSBridgeArgumentWrap::encode(bridge, method)); (maker != m_argumentFactory.end()))
return reinterpret_cast<JSBridgeArgument*>(maker->second());
return nullptr;
}
/*!
Add a factory method for constructing the arguments of a specified bridge method
@param bridge The name of the target bridge
@param method The name of the target method
*/
template<typename T> requires std::is_base_of_v<JSBridgeArgument, T>
static void defineArgument(const speckle::utility::String& bridge, const speckle::utility::String& method) {
m_argumentFactory[JSBridgeArgumentWrap::encode(bridge, method)] = std::make_pair( &typeid(T), constructArgument<T>);
}
private:
/*!
Encode bridge and method names into a single string
@param bridge The name of the target bridge
@param method The name of the target method
@return The encoded string
*/
static speckle::utility::String encode(const speckle::utility::String& bridge, const speckle::utility::String& method) {
return bridge + ":" + method;
}
//Factory function for producing instances from a serialised document object
using Production = std::function<void*()>;
///Factory functions to construct arguments from linked bridge/method names
static std::unordered_map<speckle::utility::String, Production> m_argumentFactory;
///The name of the JS object the argument is targeting
speckle::utility::String m_objectName;
///The name of the method to receive the argument
speckle::utility::String m_methodName;
///An ID to be paired with the method return value
speckle::utility::String m_requestID;
///The function arguments
std::shared_ptr<JSBridgeArgument> m_argument;
///True while the attribute are being deserialised
std::optional<bool> m_isReadingAttributes = true;
};
}
@@ -46,7 +46,7 @@ namespace speckle::interface::browser {
@param code The JS code
@return True if the code was successfully executed
*/
bool execute(const speckle::utility::String& code);
bool execute(const speckle::utility::String& code) const;
/*!
Install a JS function object
@param object The object to install
@@ -70,7 +70,7 @@ namespace speckle::interface::browser {
return: True if the code was successfully executed
--------------------------------------------------------------------*/
template<typename FunctionBinding>
bool JSPortal<FunctionBinding>::execute(const speckle::utility::String& code) {
bool JSPortal<FunctionBinding>::execute(const speckle::utility::String& code) const {
#ifdef ARCHICAD
std::shared_ptr<JavascriptEngine> engine{m_engine};
return engine ? engine->ExecuteJS(code) : false;
@@ -116,8 +116,10 @@ namespace speckle::interface::browser {
throw; //NB: Throw a system exception here in future with a defined error
if constexpr(std::is_same<Return, void>::value)
m_function(*parameters); //Parameters and no return value
else
result.reset(cloneMove(m_function(*parameters))); //Parameters with return value
else {
auto outgoing = m_function(*parameters); //Parameters with return value
result = std::move(outgoing);
}
}
return result;
} //NamedFunction<Param, Return, Packaging>::execute
@@ -142,8 +142,29 @@ namespace {
}
};
using JSElements = std::vector<std::pair<JS::Base*, String::Option>>;
using enum JSBaseIdentity::Type;
using enum JSBaseIdentity::Stage;
/*--------------------------------------------------------------------
Add an item to a JSBase object
item: The item to write
destination: The JSBase destination
--------------------------------------------------------------------*/
void addJSBase(GS::Ref<JS::Base> item, const String& tag, GS::Ref<JS::Base>& destination) {
//Attempt to add to object
if (auto object = dynamic_cast<JS::Object*>(destination.operator JS::Base*()); object != nullptr)
object->AddItem(tag, item);
//Attempt to add to array
else if (auto array = dynamic_cast<JS::Array*>(destination.operator JS::Base*()); object != nullptr)
array->AddItem(item);
else
throw std::system_error(makeJSBaseError(badDestination)); //The destination isn't a container
return;
} //addJSBase
/*--------------------------------------------------------------------
@@ -177,126 +198,104 @@ namespace {
break;
}
}
if (destination) {
auto object = JSON::ObjectValue::Cast(destination);
if (object == nullptr)
throw std::system_error(makeJSBaseError(badDestination));
object->AddValue(tag, newValue);
return;
}
destination = newValue;
if (destination)
addJSBase(newValue, tag, destination);
else
destination = newValue;
} //writeValue
/*--------------------------------------------------------------------
Decompose a JSBase into constitient items, paired with a name where possible
source: The source JSBase
return: The items in the JSBase
--------------------------------------------------------------------*/
JSElements decomposeJSBase(JS::Base& source) {
JSElements result;
if (auto object = dynamic_cast<JS::Object*>(&source); object != nullptr) {
//Decomose an object
for (auto& item : object->GetItemTable())
result.push_back({item.value->operator JS::Base*(), String{*item.key}});
} else if (auto array = dynamic_cast<JS::Array*>(&source); object != nullptr) {
//Decomose an array
for (auto& item : array->GetItemArray())
result.push_back({item, std::nullopt});
} else
throw std::system_error(makeJSBaseError(badSource)); //The source isn't a container
return result;
} //decomposeJSBase
/*--------------------------------------------------------------------
Import a cargo item from a JSBase element
cargo: A cargo item to import the phrase
source: The JSBase element to be imported
--------------------------------------------------------------------*/
void readValue(Cargo& cargo, JS::Base& source) {
auto* item = dynamic_cast<Item*>(&cargo);
if (item == nullptr)
throw std::system_error(makeJSBaseError(badValue));
} //doJSBaseItemImport
/*--------------------------------------------------------------------
Import the contents of the specified cargo from JSBase
container: The cargo container to receive the imported data
containerIdentity: The container identity
importer: The JSBase data importer
depth: The recursion depth into the JSBase hierarchy
source: The JSBase source
--------------------------------------------------------------------*/
/* void doJSBaseImport(Cargo& container, const JSBaseIdentity& containerIdentity, JSBaseImporter& importer, int32_t depth) {
if (containerIdentity.type == valueStart) {
if (auto* item = dynamic_cast<active::serialise::Item*>(&container); item != nullptr) {
importer.getContent(*item);
return;
}
void doJSBaseImport(Cargo& container, const JSBaseIdentity& containerIdentity, JS::Base& source) {
if (dynamic_cast<Item*>(&container) != nullptr) {
//If we've got a single-value item at the root, import the source value and end
readValue(container, source);
return;
}
Inventory inventory = getImportInventoryFor(container);
auto attributesRemaining = inventory.attributeSize(true); //This is tracked where the container requires attributes first
auto parsingStage = containerIdentity.stage;
auto* package = dynamic_cast<Package*>(&container);
auto isReadingAttribute = (package != nullptr) && package->isAttributeFirst();
std::optional<Memory::size_type> restorePoint;
for (;;) { //We break out of this loop when an error occurs or we run out of data
Memory::size_type readPoint = importer.getPosition();
auto identity = importer.getIdentity(parsingStage); //Get the identity of the next item in the JSBase source
switch (identity.type) {
case undefined: //End of file
if (depth != 0) //Failure if tags haven't been balanced correctly
throw std::system_error(makeJSBaseError(unbalancedScope));
return;
case delimiter:
if (parsingStage != complete) //A delimiter has been found before anything was read
throw std::system_error(makeJSBaseError(unbalancedScope));
parsingStage = containerIdentity.stage;
continue; //Move onto the next item
case objectStart: case valueStart: case arrayStart: {
if (parsingStage == complete) //An element has been read, but no delimiter reached - expected a closing symbol
throw std::system_error(makeJSBaseError(unbalancedScope));
Cargo::Unique cargo;
Inventory::iterator incomingItem = inventory.end();
bool isArrayStart = ((identity.type == arrayStart) && !identity.name.empty()), isKnown = true;
if (identity.name.empty() || isArrayStart) {
if (identity.name.empty() && parsingStage == object) //An element within an object must be identified with a name
throw std::system_error(makeJSBaseError(nameMissing));
cargo = wrapped(container); //The next element is a child (for array) or instance (for root) of the parent container
if (identity.name.empty()) {
auto incomingType = identity.type;
identity = containerIdentity; //If no name is specified, we adopt the identity specified by the container
identity.type = incomingType;
if (parsingStage == root)
identity.stage = isArrayStart ? array : object;
}
if (parsingStage == root)
cargo->setDefault(); //The root object is sourced externally, so has to be reset to the default separately
}
if (!identity.name.empty() && (parsingStage != root) && !isArrayStart) { //Allocate new cargo when a new element is reached
if (incomingItem = inventory.registerIncoming(identity); incomingItem != inventory.end()) { //Seek the incoming element in the inventory
if (isReadingAttribute && !incomingItem->isAttribute())
incomingItem = inventory.end();
else {
if (!incomingItem->bumpAvailable())
throw std::system_error(makeJSBaseError(inventoryBoundsExceeded));
if ((attributesRemaining > 0) && incomingItem->isAttribute() && incomingItem->required)
--attributesRemaining;
cargo = (incomingItem == inventory.end()) ? nullptr : container.getCargo(*incomingItem);
}
}
//Allow the parser to move beyond unknown/unwanted elements
if (!cargo) {
if (importer.isUnknownSkipped() || isReadingAttribute) {
isKnown = false;
cargo = makeUnknown(identity);
if (isReadingAttribute && !restorePoint) //If not all attributes read, parse data twice (first for attributes only)
restorePoint = readPoint; //If this is the first instance, set a restore point so reading can resume here
} else
throw std::system_error(makeJSBaseError(unknownName));
}
cargo->setDefault();
}
doJSBaseImport(*cargo, JSBaseIdentity{identity}.atStage((identity.type == arrayStart) ? array : object), importer, depth + 1);
if (incomingItem != inventory.end()) {
if (incomingItem->isRepeating()) {
if ((package != nullptr) && !package->insert(std::move(cargo), *incomingItem))
throw std::system_error(makeJSBaseError(invalidObject));
}
} else if (isKnown && !isArrayStart)
return; //If there is no defined item, we're in an array or the root - we need to return the imported element now
parsingStage = complete; //An element has been parsed - we either expect a delimiter or a terminator
break;
}
case objectEnd: case arrayEnd:
if (containerIdentity.stage != (identity.type == objectEnd ? object : array))
throw std::system_error(makeJSBaseError(unbalancedScope)); //The scope end couldn't be paired with the atart
if (restorePoint) {
isReadingAttribute = false;
importer.setPosition(*restorePoint); //Move the read position back to the first non-attribute
restorePoint.reset();
attributesRemaining = 0; //It may not be an error is this is not already zero - the container will validate the result
if (!package->finaliseAttributes())
throw std::system_error(makeJSBaseError(invalidObject));
inventory = getImportInventoryFor(container); //Having finalised attributes, the container inventory will probably change
parsingStage = object; //Resuming reading at non-attributes is always in the context of an object
break;
}
if (!container.validate())
throw std::system_error(makeJSBaseError(invalidObject)); //The incoming data was rejected as invalid
return;
}
//Find out what the container can hold
Inventory inventory;
if (!container.fillInventory(inventory))
throw std::system_error(makeJSBaseError(missingInventory));
inventory.resetAvailable(); //Reset the availability of each entry to zero so we can count incoming items
auto elements = decomposeJSBase(source);
if (elements.empty())
return;
bool isArray = !elements[0].second;
Identity parentIdentity{containerIdentity};
//Anonymous arrays need an identity
if (isArray && parentIdentity.name.empty()) {
for (auto& entry : inventory)
if (entry.isRepeating())
parentIdentity = entry.identity();
if (parentIdentity.name.empty())
throw std::system_error(makeJSBaseError(invalidObject));
}
}*/ //doJSBaseImport
for (auto& element : elements) {
Cargo::Unique cargo;
Inventory::iterator incomingItem = inventory.end();
Identity identity{element.second.value_or(parentIdentity.name)};
if (incomingItem = inventory.registerIncoming(identity); incomingItem != inventory.end()) { //Seek the incoming element in the inventory
if (!incomingItem->bumpAvailable())
throw std::system_error(makeJSBaseError(inventoryBoundsExceeded));
cargo = container.getCargo(*incomingItem);
}
if (!cargo)
continue; //TODO: Add option to throw exception for unknown elements
cargo->setDefault();
doJSBaseImport(*cargo, identity, *element.first);
if (incomingItem->isRepeating()) {
if (auto package = dynamic_cast<Package*>(&container);
(package != nullptr) && !package->insert(std::move(cargo), *incomingItem))
throw std::system_error(makeJSBaseError(invalidObject));
}
break;
}
if (!container.validate())
throw std::system_error(makeJSBaseError(invalidObject)); //The incoming data was rejected as invalid
} //doJSBaseImport
/*--------------------------------------------------------------------
@@ -307,75 +306,56 @@ namespace {
destination: The JSBase destination
--------------------------------------------------------------------*/
void doJSBaseExport(const Cargo& cargo, const JSBaseIdentity& identity, GS::Ref<JS::Base>& destination) {
String tag;
if (identity.stage != root) {
if (identity.name.empty()) //Non-root values, i.e. values embedded in an object, must have an identifying name
throw std::system_error(makeJSBaseError(nameMissing));
//Formulate and write the identifying name
tag = identity.name;
}
const auto* item = dynamic_cast<const Item*>(&cargo);
Inventory inventory;
//Single-value items won't specify an inventory (no point)
if (!cargo.fillInventory(inventory) || (inventory.empty())) {
if (item == nullptr)
throw std::system_error(makeJSBaseError(missingInventory)); //If anything other than a single-value item lands here, it's an error
writeValue(*item, tag, destination);
throw std::system_error(makeJSBaseError(badValue)); //Non-items must be named
writeValue(*item, identity.name, destination);
return;
}
if ((item != nullptr) && (inventory.size() != 1)) //An item can have multiple values but they must all be a homogenous type, e.g. an array
throw std::system_error(makeJSBaseError(badValue));
//Determine if this element acts as an object/array wrapper for values
//The package will have an outer object wrapper (even if an array) if the outer element has a name that differs from the inner item
bool isWrapper = (inventory.size() > 1) || (identity.stage == root) ||
(!identity.name.empty() && !inventory.begin()->identity().name.empty() && (inventory.begin()->identity() != identity));
//An array package will have a single item within more than one possible value
bool isArray = !isWrapper && (inventory.size() == 1) && !(inventory.begin()->maximum() == 1),
isFirstItem = true;
if (isWrapper)
exporter.writeTag(tag, nameSpace, JSBaseIdentity::Type::objectStart, depth++);
else if (isArray)
exporter.writeTag(tag, nameSpace, JSBaseIdentity::Type::arrayStart, depth);
//Determine if this cargo is a wrapper for other cargo, i.e. an object/array
bool isWrapperTag = true;
if (item != nullptr) {
if (inventory.size() != 1)
throw std::system_error(makeJSBaseError(badValue));
//Items only act as a wrapper when different (non-empty) names are defined by the inventory and the item identity
isWrapperTag = !identity.name.empty() && !inventory.begin()->identity().name.empty() && (inventory.begin()->identity() != identity);
}
auto sequence = inventory.sequence();
auto container = destination;
if (isWrapperTag) {
auto containerType = cargo.entryType().value_or((inventory.size() == 1) && !(inventory.begin()->maximum() == 1) ?
Entry::Type::array : Entry::Type::element);
if (containerType == Entry::Type::array)
container = new JS::Array();
else
container = new JS::Object();
if (destination)
addJSBase(container, identity.name, destination);
else
destination = container;
}
for (auto& entry : sequence) {
auto item = *entry.second;
if (!item.required || (item.available == 0))
if (!item.required)
continue;
if (isFirstItem)
isFirstItem = false;
else
exporter.write(",");
auto entryNameSpace{item.identity().group.value_or(String())};
//Each package item may have multiple available cargo items to export
//Each cargo container may contain multiple export items
auto limit = item.available;
bool isItemArray = item.isRepeating(),
isFirstValue = true;
if (isItemArray)
exporter.writeTag(item.identity().name, entryNameSpace, JSBaseIdentity::Type::arrayStart, depth);
for (item.available = 0; item.available < limit; ++item.available) {
auto content = cargo.getCargo(item);
if (!content)
if (auto content = cargo.getCargo(item); content) {
doJSBaseExport(*content, item.identity(), container);
} else
break; //Discontinue an inventory item when the supply runs out
if (isFirstValue)
isFirstValue = false;
else
exporter.write(",");
doJSBaseExport(*content, isItemArray ? item.identity() : JSBaseIdentity{item.identity()}.atStage(object),
exporter, (dynamic_cast<Package*>(content.get()) == nullptr) ? depth : depth + ((identity.stage == root) ? 0 : 1));
}
if (isItemArray)
exporter.writeTag(String{}, String{}, JSBaseIdentity::Type::arrayEnd, depth);
}
if (isWrapper)
exporter.writeTag(String{}, String{}, JSBaseIdentity::Type::objectEnd, --depth);
else if (isArray)
exporter.writeTag(String{}, String{}, JSBaseIdentity::Type::arrayEnd, depth);
} //doJSBaseExport
}
/*--------------------------------------------------------------------
Send cargo as XML to a specified destination
Send cargo as JSBase to a specified destination
cargo: The cargo to be sent as JS::Base
identity: The cargo identity (name, optional namespace)
@@ -387,12 +367,14 @@ void JSBaseTransport::send(Cargo&& cargo, const Identity& identity, GS::Ref<JS::
/*--------------------------------------------------------------------
Receive cargo from a specified XML source
Receive cargo from a specified JSBase source
cargo: The cargo to receive the JS::Base data
identity: The cargo identity (name, optional namespace)
source: A reference to a JS::Base object
--------------------------------------------------------------------*/
void JSBaseTransport::receive(Cargo&& cargo, const Identity& identity, GS::Ref<JS::Base> source) const {
doJSBaseImport(cargo, JSBaseIdentity(identity).atStage(root), source);
if (!source)
throw std::system_error(makeJSBaseError(badSource));
doJSBaseImport(cargo, JSBaseIdentity(identity).atStage(root), *source);
} //JSBaseTransport::receive
+9
View File
@@ -123,4 +123,13 @@ namespace speckle::utility {
}
///Hashing for String class, e.g. to use as a key in unordered_map
template <>
struct std::hash<speckle::utility::String> {
std::size_t operator()(const speckle::utility::String& k) const {
return hash<std::string>()(k); //Just use the hashing provided by std::string
}
};
#endif //SPECKLE_UTILITY_STRING
@@ -17,10 +17,11 @@
21F69EBE2C63C954008B6A06 /* Link.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69EBD2C63C954008B6A06 /* Link.cpp */; };
21F69F3B2C6B880C008B6A06 /* JSBaseTransport.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F382C6B880B008B6A06 /* JSBaseTransport.cpp */; };
21F69F512C6CCC25008B6A06 /* BrowserBridge.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F4A2C6CCC25008B6A06 /* BrowserBridge.cpp */; };
21F69F532C6CCC25008B6A06 /* JSBridgeArgument.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F4C2C6CCC25008B6A06 /* JSBridgeArgument.cpp */; };
21F69F5A2C6CDB67008B6A06 /* FunctionBinding.h in Headers */ = {isa = PBXBuildFile; fileRef = 21F69F592C6CDB67008B6A06 /* FunctionBinding.h */; };
21F69F612C6D0286008B6A06 /* GetBindingsMethodNames.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F602C6D0286008B6A06 /* GetBindingsMethodNames.cpp */; };
21F69F682C6DFB01008B6A06 /* RunMethod.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F662C6DFB01008B6A06 /* RunMethod.cpp */; };
21F69F7E2C6FD9FC008B6A06 /* GetCallResult.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F7A2C6FD9FC008B6A06 /* GetCallResult.cpp */; };
21F69F812C6FF3B0008B6A06 /* JSBridgeArgumentWrap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F69F802C6FF3B0008B6A06 /* JSBridgeArgumentWrap.cpp */; };
21F93AEC2B2F406E009A2C5B /* Addon.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 21F93AEA2B2F406D009A2C5B /* Addon.cpp */; };
/* End PBXBuildFile section */
@@ -98,7 +99,6 @@
21F69F492C6CC2B8008B6A06 /* Functional.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Functional.h; sourceTree = "<group>"; };
21F69F4A2C6CCC25008B6A06 /* BrowserBridge.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BrowserBridge.cpp; sourceTree = "<group>"; };
21F69F4B2C6CCC25008B6A06 /* BrowserBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrowserBridge.h; sourceTree = "<group>"; };
21F69F4C2C6CCC25008B6A06 /* JSBridgeArgument.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSBridgeArgument.cpp; sourceTree = "<group>"; };
21F69F4D2C6CCC25008B6A06 /* JSBridgeArgument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSBridgeArgument.h; sourceTree = "<group>"; };
21F69F4F2C6CCC25008B6A06 /* JSBridgeMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSBridgeMethod.h; sourceTree = "<group>"; };
21F69F572C6CDAEE008B6A06 /* GetBindingsMethodNames.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GetBindingsMethodNames.h; sourceTree = "<group>"; };
@@ -109,6 +109,9 @@
21F69F692C6E0D59008B6A06 /* JSBridgeArgumentWrap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSBridgeArgumentWrap.h; sourceTree = "<group>"; };
21F69F6A2C6E61E1008B6A06 /* JSPortal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSPortal.h; sourceTree = "<group>"; };
21F69F6D2C6E7D9F008B6A06 /* PlatformBinding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlatformBinding.h; sourceTree = "<group>"; };
21F69F7A2C6FD9FC008B6A06 /* GetCallResult.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GetCallResult.cpp; sourceTree = "<group>"; };
21F69F7D2C6FD9FC008B6A06 /* GetCallResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GetCallResult.h; sourceTree = "<group>"; };
21F69F802C6FF3B0008B6A06 /* JSBridgeArgumentWrap.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSBridgeArgumentWrap.cpp; sourceTree = "<group>"; };
21F93AE92B2F406D009A2C5B /* Addon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Addon.h; sourceTree = "<group>"; };
21F93AEA2B2F406D009A2C5B /* Addon.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Addon.cpp; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -290,8 +293,8 @@
21F69F4A2C6CCC25008B6A06 /* BrowserBridge.cpp */,
21F69F4B2C6CCC25008B6A06 /* BrowserBridge.h */,
21F69F582C6CDAEE008B6A06 /* Functions */,
21F69F4C2C6CCC25008B6A06 /* JSBridgeArgument.cpp */,
21F69F4D2C6CCC25008B6A06 /* JSBridgeArgument.h */,
21F69F802C6FF3B0008B6A06 /* JSBridgeArgumentWrap.cpp */,
21F69F692C6E0D59008B6A06 /* JSBridgeArgumentWrap.h */,
21F69F4F2C6CCC25008B6A06 /* JSBridgeMethod.h */,
);
@@ -304,6 +307,8 @@
21F69F592C6CDB67008B6A06 /* FunctionBinding.h */,
21F69F602C6D0286008B6A06 /* GetBindingsMethodNames.cpp */,
21F69F572C6CDAEE008B6A06 /* GetBindingsMethodNames.h */,
21F69F7A2C6FD9FC008B6A06 /* GetCallResult.cpp */,
21F69F7D2C6FD9FC008B6A06 /* GetCallResult.h */,
21F69F662C6DFB01008B6A06 /* RunMethod.cpp */,
21F69F652C6DFB01008B6A06 /* RunMethod.h */,
);
@@ -464,11 +469,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
21F69F532C6CCC25008B6A06 /* JSBridgeArgument.cpp in Sources */,
21F69F682C6DFB01008B6A06 /* RunMethod.cpp in Sources */,
21F69F812C6FF3B0008B6A06 /* JSBridgeArgumentWrap.cpp in Sources */,
2193517B2C624FC100E5A69C /* MenuSubscriber.cpp in Sources */,
21F69F612C6D0286008B6A06 /* GetBindingsMethodNames.cpp in Sources */,
21F93AEC2B2F406E009A2C5B /* Addon.cpp in Sources */,
21F69F7E2C6FD9FC008B6A06 /* GetCallResult.cpp in Sources */,
2193519B2C6278D900E5A69C /* SelectionSubscriber.cpp in Sources */,
21F69EBE2C63C954008B6A06 /* Link.cpp in Sources */,
219351B32C62CC1A00E5A69C /* String.cpp in Sources */,