diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.cpp b/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.cpp index bd895da..4a8c5d1 100644 --- a/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.cpp +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.cpp @@ -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> { + class BrowserBridge::ResultCache : public std::map> { 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(*this)); emplace_back(std::make_unique(*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 result, speckle::utility::String requestID) { +void BrowserBridge::cacheResult(std::unique_ptr result, String requestID) { + if (m_portal == nullptr) + throw; //TODO: Add exception detail const std::lock_guard 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 BrowserBridge::releaseResult(String requestID) { + std::unique_ptr 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 diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.h b/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.h index 9d7c705..890c3c1 100644 --- a/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.h +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/BrowserBridge.h @@ -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 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 releaseResult(speckle::utility::String requestID); protected: /*! diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/Functions/GetCallResult.cpp b/SpeckleLib/Speckle/Interface/Browser/Bridge/Functions/GetCallResult.cpp new file mode 100644 index 0000000..609456f --- /dev/null +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/Functions/GetCallResult.cpp @@ -0,0 +1,35 @@ +#include "Speckle/Interface/Browser/Bridge/Functions/GetCallResult.h" + +#include "Speckle/Interface/Browser/Bridge/BrowserBridge.h" + +#ifdef ARCHICAD +#include +#include +#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 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 diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/Functions/GetCallResult.h b/SpeckleLib/Speckle/Interface/Browser/Bridge/Functions/GetCallResult.h new file mode 100644 index 0000000..11c7339 --- /dev/null +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/Functions/GetCallResult.h @@ -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 { + 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 getArgument() const override; + + private: + /*! + Run a specified bridge method + @param argument The method arguments + */ + std::unique_ptr runMethod(JSBridgeArgumentWrap& argument) const; + + ///The parent browser bridge + BrowserBridge& m_bridge; + }; + +} + +#endif //SPECKLE_INTERFACE_BRIDGE_GET_CALL_RESULT diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.cpp b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.cpp deleted file mode 100644 index b096643..0000000 --- a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.cpp +++ /dev/null @@ -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>(m_objectName); - case FieldIndex::methodName: - return std::make_unique>(m_methodName); - case FieldIndex::requestID: - return std::make_unique>(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 diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.h b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.h index 6107618..a78a30f 100644 --- a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.h +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgument.h @@ -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 diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.cpp b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.cpp new file mode 100644 index 0000000..f6cdb44 --- /dev/null +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.cpp @@ -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>(m_objectName); + case FieldIndex::methodName: + return std::make_unique>(m_methodName); + case FieldIndex::requestID: + return std::make_unique>(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(m_objectName, m_methodName, m_requestID); + return true; +} //JSBridgeArgumentWrap::finaliseAttributes diff --git a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.h b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.h index 6efaf61..3d123d4 100644 --- a/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.h +++ b/SpeckleLib/Speckle/Interface/Browser/Bridge/JSBridgeArgumentWrap.h @@ -4,9 +4,24 @@ #include "Active/Serialise/Package/Package.h" #include "Speckle/Interface/Browser/Bridge/JSBridgeArgument.h" +#include + namespace speckle::interface::browser::bridge { class JSBridgeArgument; + + /*! + Factory function to make an argument object + @return A new argument object + */ + template + 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(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 requires std::is_base_of_v + 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); + } 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; + ///Factory functions to construct arguments from linked bridge/method names + static std::unordered_map 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 m_argument; + ///True while the attribute are being deserialised + std::optional m_isReadingAttributes = true; }; } diff --git a/SpeckleLib/Speckle/Interface/Browser/JSPortal.h b/SpeckleLib/Speckle/Interface/Browser/JSPortal.h index ec8a6ec..d6a73a8 100644 --- a/SpeckleLib/Speckle/Interface/Browser/JSPortal.h +++ b/SpeckleLib/Speckle/Interface/Browser/JSPortal.h @@ -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 - bool JSPortal::execute(const speckle::utility::String& code) { + bool JSPortal::execute(const speckle::utility::String& code) const { #ifdef ARCHICAD std::shared_ptr engine{m_engine}; return engine ? engine->ExecuteJS(code) : false; diff --git a/SpeckleLib/Speckle/Interface/Browser/NamedFunction.h b/SpeckleLib/Speckle/Interface/Browser/NamedFunction.h index 5bc10e6..89015e9 100644 --- a/SpeckleLib/Speckle/Interface/Browser/NamedFunction.h +++ b/SpeckleLib/Speckle/Interface/Browser/NamedFunction.h @@ -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::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::execute diff --git a/SpeckleLib/Speckle/Serialise/JSBase/JSBaseTransport.cpp b/SpeckleLib/Speckle/Serialise/JSBase/JSBaseTransport.cpp index 7305205..ecf1d8e 100644 --- a/SpeckleLib/Speckle/Serialise/JSBase/JSBaseTransport.cpp +++ b/SpeckleLib/Speckle/Serialise/JSBase/JSBaseTransport.cpp @@ -142,8 +142,29 @@ namespace { } }; + using JSElements = std::vector>; + 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 item, const String& tag, GS::Ref& destination) { + //Attempt to add to object + if (auto object = dynamic_cast(destination.operator JS::Base*()); object != nullptr) + object->AddItem(tag, item); + //Attempt to add to array + else if (auto array = dynamic_cast(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(&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(&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(&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(&container); item != nullptr) { - importer.getContent(*item); - return; - } + void doJSBaseImport(Cargo& container, const JSBaseIdentity& containerIdentity, JS::Base& source) { + if (dynamic_cast(&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(&container); - auto isReadingAttribute = (package != nullptr) && package->isAttributeFirst(); - std::optional 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(&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& 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(&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(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 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 diff --git a/SpeckleLib/Speckle/Utility/String.h b/SpeckleLib/Speckle/Utility/String.h index 2f765b1..539ae3d 100644 --- a/SpeckleLib/Speckle/Utility/String.h +++ b/SpeckleLib/Speckle/Utility/String.h @@ -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 { + std::size_t operator()(const speckle::utility::String& k) const { + return hash()(k); //Just use the hashing provided by std::string + } +}; + + #endif //SPECKLE_UTILITY_STRING diff --git a/SpeckleLib/SpeckleLib.xcodeproj/project.pbxproj b/SpeckleLib/SpeckleLib.xcodeproj/project.pbxproj index 500b00f..25bbf89 100644 --- a/SpeckleLib/SpeckleLib.xcodeproj/project.pbxproj +++ b/SpeckleLib/SpeckleLib.xcodeproj/project.pbxproj @@ -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 = ""; }; 21F69F4A2C6CCC25008B6A06 /* BrowserBridge.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BrowserBridge.cpp; sourceTree = ""; }; 21F69F4B2C6CCC25008B6A06 /* BrowserBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrowserBridge.h; sourceTree = ""; }; - 21F69F4C2C6CCC25008B6A06 /* JSBridgeArgument.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSBridgeArgument.cpp; sourceTree = ""; }; 21F69F4D2C6CCC25008B6A06 /* JSBridgeArgument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSBridgeArgument.h; sourceTree = ""; }; 21F69F4F2C6CCC25008B6A06 /* JSBridgeMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSBridgeMethod.h; sourceTree = ""; }; 21F69F572C6CDAEE008B6A06 /* GetBindingsMethodNames.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GetBindingsMethodNames.h; sourceTree = ""; }; @@ -109,6 +109,9 @@ 21F69F692C6E0D59008B6A06 /* JSBridgeArgumentWrap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSBridgeArgumentWrap.h; sourceTree = ""; }; 21F69F6A2C6E61E1008B6A06 /* JSPortal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSPortal.h; sourceTree = ""; }; 21F69F6D2C6E7D9F008B6A06 /* PlatformBinding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlatformBinding.h; sourceTree = ""; }; + 21F69F7A2C6FD9FC008B6A06 /* GetCallResult.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GetCallResult.cpp; sourceTree = ""; }; + 21F69F7D2C6FD9FC008B6A06 /* GetCallResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GetCallResult.h; sourceTree = ""; }; + 21F69F802C6FF3B0008B6A06 /* JSBridgeArgumentWrap.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSBridgeArgumentWrap.cpp; sourceTree = ""; }; 21F93AE92B2F406D009A2C5B /* Addon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Addon.h; sourceTree = ""; }; 21F93AEA2B2F406D009A2C5B /* Addon.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Addon.cpp; sourceTree = ""; }; /* 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 */,