diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/CMakeLists.txt | 2 | ||||
-rw-r--r-- | core/src/Model/EvaluatedValue.cpp | 49 | ||||
-rw-r--r-- | core/src/Model/EvaluatedValue.hpp | 25 | ||||
-rw-r--r-- | core/src/Model/Workflow.cpp | 306 | ||||
-rw-r--r-- | core/src/Model/Workflow.hpp | 175 | ||||
-rw-r--r-- | core/src/Model/WorkflowNodes.cpp | 309 | ||||
-rw-r--r-- | core/src/Model/WorkflowNodes.hpp | 101 | ||||
-rw-r--r-- | core/src/Model/WorkflowSteps.cpp | 11 | ||||
-rw-r--r-- | core/src/Model/WorkflowSteps.hpp | 44 | ||||
-rw-r--r-- | core/src/Model/fwd.hpp | 15 | ||||
-rw-r--r-- | core/src/Utils/Macros.hpp | 8 | ||||
-rw-r--r-- | core/src/Utils/RTTI.hpp | 44 | ||||
-rw-r--r-- | core/src/Utils/Variant.hpp | 27 |
13 files changed, 940 insertions, 176 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 12361f3..b6199cd 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -40,7 +40,7 @@ add_source_group(MODEL_MODULE_SOURCES src/Model/Project.cpp src/Model/TransactionsModel.cpp src/Model/Workflow.cpp - src/Model/WorkflowSteps.cpp + src/Model/WorkflowNodes.cpp ) add_source_group(UI_MODULE_SOURCES diff --git a/core/src/Model/EvaluatedValue.cpp b/core/src/Model/EvaluatedValue.cpp index c86f00b..685d50f 100644 --- a/core/src/Model/EvaluatedValue.cpp +++ b/core/src/Model/EvaluatedValue.cpp @@ -1,15 +1,32 @@ #include "EvaluatedValue.hpp" -BaseValue::BaseValue(Type type) - : mType{ type } { +#include <charconv> + +BaseValue::BaseValue(Kind kind) + : mKind{ kind } { +} + +BaseValue::Kind BaseValue::GetKind() const { + return mKind; } -BaseValue::Type BaseValue::GetType() const { - return mType; +bool NumericValue::IsInstance(const BaseValue* value) { + return value->GetKind() == KD_Numeric; } NumericValue::NumericValue() - : BaseValue(BaseValue::NumericType) { + : BaseValue(BaseValue::KD_Numeric) { +} + +std::string NumericValue::GetString() const { + char buf[64]; + auto res = std::to_chars(buf, buf + std::size(buf), mValue); + if (res.ec == std::errc()) { + return std::string(buf, res.ptr); + } else { + // TODO larger buffer + return "<err>"; + } } int64_t NumericValue::GetInt() const { @@ -24,8 +41,12 @@ void NumericValue::SetValue(double value) { mValue = value; } +bool TextValue::IsInstance(const BaseValue* value) { + return value->GetKind() == KD_Text; +} + TextValue::TextValue() - : BaseValue(BaseValue::TextType) { + : BaseValue(BaseValue::KD_Text) { } const std::string& TextValue::GetValue() const { @@ -36,8 +57,22 @@ void TextValue::SetValue(const std::string& value) { mValue = value; } +bool DateTimeValue::IsInstance(const BaseValue* value) { + return value->GetKind() == KD_DateTime; +} + DateTimeValue::DateTimeValue() - : BaseValue(BaseValue::DateTimeType) { + : BaseValue(BaseValue::KD_DateTime) { +} + +std::string DateTimeValue::GetString() const { + namespace chrono = std::chrono; + auto t = chrono::system_clock::to_time_t(mValue); + + char data[32]; + std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); + + return std::string(data); } const std::chrono::time_point<std::chrono::system_clock>& DateTimeValue::GetValue() const { diff --git a/core/src/Model/EvaluatedValue.hpp b/core/src/Model/EvaluatedValue.hpp index 838fc39..880fd3e 100644 --- a/core/src/Model/EvaluatedValue.hpp +++ b/core/src/Model/EvaluatedValue.hpp @@ -6,21 +6,21 @@ class BaseValue { public: - enum Type { - NumericType, - TextType, - DateTimeType, + enum Kind { + KD_Numeric, + KD_Text, + KD_DateTime, /// An unspecified type, otherwise known as "any" in some contexts. - InvalidType, - TypeCount = InvalidType, + KindInvalid, + KindCount = KindInvalid, }; private: - Type mType; + Kind mKind; public: - BaseValue(Type type); + BaseValue(Kind kind); virtual ~BaseValue() = default; BaseValue(const BaseValue&) = delete; @@ -28,7 +28,7 @@ public: BaseValue(BaseValue&&) = default; BaseValue& operator=(BaseValue&&) = default; - Type GetType() const; + Kind GetKind() const; }; class NumericValue : public BaseValue { @@ -36,8 +36,10 @@ private: double mValue; public: + static bool IsInstance(const BaseValue* value); NumericValue(); + std::string GetString() const; int64_t GetInt() const; double GetValue() const; void SetValue(double value); @@ -48,7 +50,9 @@ private: std::string mValue; public: + static bool IsInstance(const BaseValue* value); TextValue(); + const std::string& GetValue() const; void SetValue(const std::string& value); }; @@ -58,7 +62,10 @@ private: std::chrono::time_point<std::chrono::system_clock> mValue; public: + static bool IsInstance(const BaseValue* value); DateTimeValue(); + + std::string GetString() const; const std::chrono::time_point<std::chrono::system_clock>& GetValue() const; void SetValue(const std::chrono::time_point<std::chrono::system_clock>& value); }; diff --git a/core/src/Model/Workflow.cpp b/core/src/Model/Workflow.cpp index 96031c1..385544e 100644 --- a/core/src/Model/Workflow.cpp +++ b/core/src/Model/Workflow.cpp @@ -1,98 +1,298 @@ #include "Workflow.hpp" -bool WorkflowStep::InputNode::IsConnected() const { - return ConnectedNodeIndex != -1; +#include <cassert> +#include <utility> + +WorkflowConnection::WorkflowConnection() + : Source{ WorkflowNode::kInvalidId } + , Destination{ WorkflowNode::kInvalidId } + , SourcePin{ -1 } + , DestinationPin{ -1 } { +} + +bool WorkflowConnection::IsValid() const { + return Source != WorkflowNode::kInvalidId; +} + +bool WorkflowNode::InputPin::IsConstantConnection() const { + return ConnectionToConst; } -bool WorkflowStep::OutputNode::IsConnected() const { - return ConnectedNodeIndex != -1; +bool WorkflowNode::InputPin::IsConnected() const { + return Connection != WorkflowConnection::kInvalidId; } -size_t WorkflowStep::GetId() const { +bool WorkflowNode::OutputPin::IsConnected() const { + return Connection != WorkflowConnection::kInvalidId; +} + +WorkflowNode::WorkflowNode(Type type, Kind kind) + : mType{ type } + , mKind{ kind } { +} + +size_t WorkflowNode::GetId() const { return mId; } -void WorkflowStep::ConnectInput(int nodeId, size_t outputId, int outputNodeId) { - mWorkflow->Connect(mId, nodeId, outputId, outputNodeId); +WorkflowNode::Type WorkflowNode::GetType() const { + return mType; +} + +WorkflowNode::Kind WorkflowNode::GetKind() const { + return mKind; +} + +void WorkflowNode::ConnectInput(int nodeId, WorkflowNode& output, int outputNodeId) { + mWorkflow->Connect(*this, nodeId, output, outputNodeId); } -void WorkflowStep::DisconnectInput(int nodeId) { - mWorkflow->DisconnectByDestination(mId, nodeId); +void WorkflowNode::DisconnectInput(int nodeId) { + mWorkflow->DisconnectByDestination(*this, nodeId); } -bool WorkflowStep::IsInputConnected(int nodeId) const { +bool WorkflowNode::IsInputConnected(int nodeId) const { return mInputs[nodeId].IsConnected(); } -void WorkflowStep::ConnectOutput(int nodeId, size_t inputId, int inputNodeId) { - mWorkflow->Connect(inputId, inputNodeId, mId, nodeId); +void WorkflowNode::ConnectOutput(int nodeId, WorkflowNode& input, int inputNodeId) { + mWorkflow->Connect(input, inputNodeId, *this, nodeId); } -void WorkflowStep::DisconnectOutput(int nodeId) { - mWorkflow->DisconnectBySource(mId, nodeId); +void WorkflowNode::DisconnectOutput(int nodeId) { + mWorkflow->DisconnectBySource(*this, nodeId); } -bool WorkflowStep::IsOutputConnected(int nodeId) const { +bool WorkflowNode::IsOutputConnected(int nodeId) const { return mOutputs[nodeId].IsConnected(); } +WorkflowNode::InputPin& WorkflowNode::InsertInputPin(int atIdx) { + assert(atIdx >= 0 && atIdx < mInputs.size()); + + mInputs.push_back(InputPin{}); + for (int i = (int)mInputs.size() - 1, end = atIdx + 1; i >= end; --i) { + SwapInputPin(i, i + 1); + } + + return mInputs[atIdx]; +} + +void WorkflowNode::RemoveInputPin(int pin) { + DisconnectInput(pin); + for (int i = 0, end = (int)mInputs.size() - 1; i < end; ++i) { + SwapInputPin(i, i + 1); + } + mInputs.resize(mInputs.size() - 1); +} + +void WorkflowNode::SwapInputPin(int a, int b) { + auto& pinA = mInputs[a]; + auto& pinB = mInputs[b]; + + if (mWorkflow) { + if (pinA.IsConnected() && !pinA.IsConstantConnection()) { + auto& conn = *mWorkflow->GetConnectionById(pinA.Connection); + conn.DestinationPin = b; + } + if (pinB.IsConnected() && !pinB.IsConstantConnection()) { + auto& conn = *mWorkflow->GetConnectionById(pinB.Connection); + conn.DestinationPin = a; + } + } + + std::swap(pinA, pinB); +} + +WorkflowNode::OutputPin& WorkflowNode::InsertOutputPin(int atIdx) { + assert(atIdx >= 0 && atIdx < mOutputs.size()); + + mOutputs.push_back(OutputPin{}); + for (int i = (int)mOutputs.size() - 1, end = atIdx + 1; i >= end; --i) { + SwapOutputPin(i, i + 1); + } + + return mOutputs[atIdx]; +} + +void WorkflowNode::RemoveOutputPin(int pin) { + DisconnectOutput(pin); + for (int i = 0, end = (int)mOutputs.size() - 1; i < end; ++i) { + SwapInputPin(i, i + 1); + } + mOutputs.resize(mOutputs.size() - 1); +} + +void WorkflowNode::SwapOutputPin(int a, int b) { + auto& pinA = mOutputs[a]; + auto& pinB = mOutputs[b]; + + if (mWorkflow) { + if (pinA.IsConnected()) { + auto& conn = *mWorkflow->GetConnectionById(pinA.Connection); + conn.SourcePin = b; + } + if (pinB.IsConnected()) { + auto& conn = *mWorkflow->GetConnectionById(pinB.Connection); + conn.SourcePin = a; + } + } + + std::swap(pinA, pinB); +} + WorkflowConnection* Workflow::GetConnectionById(size_t id) { return &mConnections[id]; } -WorkflowStep* Workflow::GetStepById(size_t id) { - return mSteps[id].get(); +WorkflowNode* Workflow::GetStepById(size_t id) { + return mNodes[id].get(); +} + +BaseValue* Workflow::GetConstantById(size_t id) { + return mConstants[id].get(); +} + +void Workflow::AddStep(std::unique_ptr<WorkflowNode> step) { + auto [storage, id] = AllocWorkflowStep(); + storage = std::move(step); + storage->OnAttach(*this, id); + storage->mWorkflow = this; + storage->mId = id; +} + +void Workflow::RemoveStep(size_t id) { + auto& step = mNodes[id]; + if (step == nullptr) return; + + step->OnDetach(); + step->mWorkflow = nullptr; + step->mId = WorkflowNode::kInvalidId; +} + +void Workflow::Connect(WorkflowNode& source, int sourceNode, WorkflowNode& destination, int destinationNode) { + if (source.mInputs[sourceNode].IsConnected()) { + DisconnectBySource(source, sourceNode); + } + + auto [conn, id] = AllocWorkflowConnection(); + conn.Source = source.GetId(); + conn.SourcePin = sourceNode; + conn.Destination = destination.GetId(); + conn.DestinationPin = destinationNode; + + source.mInputs[sourceNode].Connection = id; + destination.mInputs[destinationNode].Connection = id; +} + +void Workflow::DisconnectBySource(WorkflowNode& source, int sourceNode) { + auto& sn = source.mOutputs[sourceNode]; + if (!sn.IsConnected()) return; + + auto& conn = mConnections[sn.Connection]; + auto& dn = mNodes[conn.Destination]->mInputs[conn.DestinationPin]; + + sn.Connection = WorkflowConnection::kInvalidId; + dn.Connection = WorkflowConnection::kInvalidId; + conn = {}; +} + +void Workflow::DisconnectByDestination(WorkflowNode& destination, int destinationNode) { + auto& dn = destination.mOutputs[destinationNode]; + if (!dn.IsConnected()) return; + + auto& conn = mConnections[dn.Connection]; + auto& sn = mNodes[conn.Source]->mInputs[conn.SourcePin]; + + sn.Connection = WorkflowConnection::kInvalidId; + dn.Connection = WorkflowConnection::kInvalidId; + conn = {}; +} + +std::pair<WorkflowConnection&, size_t> Workflow::AllocWorkflowConnection() { + for (size_t idx = 0; idx < mConnections.size(); ++idx) { + auto& elm = mConnections[idx]; + if (!elm.IsValid()) { + return { elm, idx }; + } + } + + auto id = mConnections.size(); + return { mConnections.emplace_back(WorkflowConnection{}), id }; +} + +std::pair<std::unique_ptr<WorkflowNode>&, size_t> Workflow::AllocWorkflowStep() { + for (size_t idx = 0; idx < mNodes.size(); ++idx) { + auto& elm = mNodes[idx]; + if (elm == nullptr) { + return { elm, idx }; + } + } + + auto id = mNodes.size(); + return { mNodes.emplace_back(std::unique_ptr<WorkflowNode>()), id }; } -void WorkflowStep::OnIOAboutToChange() { - // TODO more robust solution that handles changes incrementally - for (size_t i = 0; i < mInputs.size(); ++i) { - DisconnectInput(i); +struct WorkflowEvaluationContext::RuntimeNode { + WorkflowNode* Node; +}; + +struct WorkflowEvaluationContext::RuntimeConnection { + WorkflowConnection* Connection; + std::unique_ptr<BaseValue> Value; +}; + +WorkflowEvaluationContext::WorkflowEvaluationContext(Workflow& workflow) + : mWorkflow{ &workflow } { + mNodes.resize(workflow.mNodes.size()); + for (size_t i = 0; i < workflow.mNodes.size(); ++i) { + mNodes[i].Node = workflow.mNodes[i].get(); } - for (size_t i = 0; i < mOutputs.size(); ++i) { - DisconnectOutput(i); + + mConnections.resize(workflow.mConnections.size()); + for (size_t i = 0; i < workflow.mConnections.size(); ++i) { + mConnections[i].Connection = &workflow.mConnections[i]; } } -void WorkflowStep::OnIOChanged() { +BaseValue* WorkflowEvaluationContext::GetConnectionValue(size_t id, bool constant) { + if (constant) { + return mWorkflow->GetConstantById(id); + } else { + return mConnections[id].Value.get(); + } } -void Workflow::Connect(size_t source, int sourceNode, size_t destination, int destinationNode) { - auto& src = *mSteps[source]; - auto& o = src.mInputs[sourceNode]; - if (o.IsConnected()) { - DisconnectBySource(source, sourceNode); +BaseValue* WorkflowEvaluationContext::GetConnectionValue(const WorkflowNode::InputPin& inputPin) { + if (inputPin.IsConnected()) { + return GetConnectionValue(inputPin.Connection, inputPin.IsConstantConnection()); + } else { + return nullptr; } +} + +void WorkflowEvaluationContext::SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value) { + mConnections[id].Value = std::move(value); +} - o.ConnectedStep = destination; - o.ConnectedNodeIndex = destinationNode; +void WorkflowEvaluationContext::SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value) { + if (outputPin.IsConnected()) { + SetConnectionValue(outputPin.Connection, std::move(value)); + } +} - auto& dst = *mSteps[destination]; - auto& i = dst.mInputs[destinationNode]; - i.ConnectedStep = source; - i.ConnectedNodeIndex = sourceNode; +void WorkflowEvaluationContext::Run() { + // TODO } -void Workflow::DisconnectBySource(size_t source, int sourceNode) { - auto& src = *mSteps[source]; - auto& o = src.mOutputs[sourceNode]; - if (!o.IsConnected()) return; +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin) { +} - auto& i = mSteps[o.ConnectedStep]->mInputs[o.ConnectedNodeIndex]; - i.ConnectedStep = WorkflowStep::kInvalidId; - i.ConnectedNodeIndex = -1; - o.ConnectedStep = WorkflowStep::kInvalidId; - o.ConnectedNodeIndex = -1; +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node) { } -void Workflow::DisconnectByDestination(size_t destination, int destinationNode) { - auto& dst = *mSteps[destination]; - auto& i = dst.mInputs[destinationNode]; - if (!i.IsConnected()) return; +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin) { +} - auto& o = mSteps[i.ConnectedStep]->mOutputs[i.ConnectedNodeIndex]; - i.ConnectedStep = WorkflowStep::kInvalidId; - i.ConnectedNodeIndex = -1; - o.ConnectedStep = WorkflowStep::kInvalidId; - o.ConnectedNodeIndex = -1; +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node) { } diff --git a/core/src/Model/Workflow.hpp b/core/src/Model/Workflow.hpp index a90ef21..2c83816 100644 --- a/core/src/Model/Workflow.hpp +++ b/core/src/Model/Workflow.hpp @@ -1,6 +1,7 @@ #pragma once -#include "EvaluatedValue.hpp" +#include "Model/EvaluatedValue.hpp" +#include "cplt_fwd.hpp" #include <cstddef> #include <cstdint> @@ -10,92 +11,180 @@ class WorkflowConnection { public: + static constexpr auto kInvalidId = std::numeric_limits<size_t>::max(); + size_t Source; size_t Destination; + int SourcePin; + int DestinationPin; + +public: + WorkflowConnection(); + + bool IsValid() const; }; -class WorkflowStep { +class WorkflowNode { public: static constexpr auto kInvalidId = std::numeric_limits<size_t>::max(); - // TODO do we even need this? - // enum Type { - // NumericAdditionType, - // NumericSubtractionType, - // NumericMultiplicationType, - // NumericDivisionType, - // NumericExpressionType, - // TextFormattingType, - // FormInputType, - // DatabaseRowsInputType, - // DocumentTemplateExpansionType, - // - // InvalidType, - // TypeCount = InvalidType, - // }; + enum Type { + InputType, + TransformType, + OutputType, + }; + + enum Kind { + KD_NumericAddition, + KD_NumericSubtraction, + KD_NumericMultiplication, + KD_NumericDivision, + KD_NumericExpression, + KD_TextFormatting, + KD_DocumentTemplateExpansion, + KD_FormInput, + KD_DatabaseRowsInput, + + InvalidKind, + KindCount = InvalidKind, + }; protected: - struct InputNode { - size_t ConnectedStep = kInvalidId; - int ConnectedNodeIndex = -1; - BaseValue::Type MatchingType = BaseValue::InvalidType; + struct InputPin { + size_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::KindInvalid; + bool ConnectionToConst = false; + /// A constant connection connects from a user-specified constant value, feeding to a valid \c Destination and \c DestinationPin (i.e. input pins). + bool IsConstantConnection() const; bool IsConnected() const; }; - struct OutputNode { - size_t ConnectedStep = kInvalidId; - int ConnectedNodeIndex = -1; - BaseValue::Type MatchingType = BaseValue::InvalidType; + struct OutputPin { + size_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::KindInvalid; bool IsConnected() const; }; friend class Workflow; + friend class WorkflowEvaluationError; + friend class WorkflowEvaluationContext; + Workflow* mWorkflow; size_t mId; - std::vector<InputNode> mInputs; - std::vector<OutputNode> mOutputs; + std::vector<InputPin> mInputs; + std::vector<OutputPin> mOutputs; + Type mType; + Kind mKind; public: - virtual ~WorkflowStep() = default; + WorkflowNode(Type type, Kind kind); + virtual ~WorkflowNode() = default; - WorkflowStep(const WorkflowStep&) = delete; - WorkflowStep& operator=(const WorkflowStep&) = delete; - WorkflowStep(WorkflowStep&&) = default; - WorkflowStep& operator=(WorkflowStep&&) = default; + WorkflowNode(const WorkflowNode&) = delete; + WorkflowNode& operator=(const WorkflowNode&) = delete; + WorkflowNode(WorkflowNode&&) = default; + WorkflowNode& operator=(WorkflowNode&&) = default; size_t GetId() const; + Type GetType() const; + Kind GetKind() const; - void ConnectInput(int nodeId, size_t outputId, int outputNodeId); + void ConnectInput(int nodeId, WorkflowNode& output, int outputNodeId); void DisconnectInput(int nodeId); bool IsInputConnected(int nodeId) const; - void ConnectOutput(int nodeId, size_t inputId, int inputNodeId); + void ConnectOutput(int nodeId, WorkflowNode& input, int inputNodeId); void DisconnectOutput(int nodeId); bool IsOutputConnected(int nodeId) const; + virtual void Evaluate(WorkflowEvaluationContext& ctx) = 0; + protected: - /// Child classes should call this whenever \c mInputs and \c mOutputs are about to be modified (append or remove) - /// after invocation of the constructor. - void OnIOAboutToChange(); - void OnIOChanged(); + InputPin& InsertInputPin(int atIdx); + void RemoveInputPin(int pin); + void SwapInputPin(int a, int b); + OutputPin& InsertOutputPin(int atIdx); + void RemoveOutputPin(int pin); + void SwapOutputPin(int a, int b); + + /* For \c Workflow to invoke, override by implementations */ + + void OnAttach(Workflow& workflow, size_t newId) {} + void OnDetach() {} }; class Workflow { private: + friend class WorkflowEvaluationContext; + std::vector<WorkflowConnection> mConnections; - std::vector<std::unique_ptr<WorkflowStep>> mSteps; + std::vector<std::unique_ptr<WorkflowNode>> mNodes; + std::vector<std::unique_ptr<BaseValue>> mConstants; public: WorkflowConnection* GetConnectionById(size_t id); - WorkflowStep* GetStepById(size_t id); + WorkflowNode* GetStepById(size_t id); + BaseValue* GetConstantById(size_t id); + + void AddStep(std::unique_ptr<WorkflowNode> step); + void RemoveStep(size_t id); + + void Connect(WorkflowNode& source, int sourceNode, WorkflowNode& destination, int destinationNode); + void DisconnectBySource(WorkflowNode& source, int sourceNode); + void DisconnectByDestination(WorkflowNode& destination, int destinationNode); - void Connect(size_t source, int sourceNode, size_t destination, int destinationNode); - void DisconnectBySource(size_t source, int sourceNode); - void DisconnectByDestination(size_t destination, int destinationNode); +private: + std::pair<WorkflowConnection&, size_t> AllocWorkflowConnection(); + std::pair<std::unique_ptr<WorkflowNode>&, size_t> AllocWorkflowStep(); +}; + +class WorkflowEvaluationError { +public: + enum MessageType : int16_t { + Error, + Warning, + }; + + enum PinType : int16_t { + NoPin, + InputPin, + OutputPin, + }; + +public: + std::string Message; + size_t NodeId; + int PinId; + PinType PinType; + MessageType Type; }; class WorkflowEvaluationContext { +private: + struct RuntimeNode; + struct RuntimeConnection; + + Workflow* mWorkflow; + std::vector<RuntimeNode> mNodes; + std::vector<RuntimeConnection> mConnections; + std::vector<WorkflowEvaluationError> mErrors; + std::vector<WorkflowEvaluationError> mWarnings; + +public: + WorkflowEvaluationContext(Workflow& workflow); + + BaseValue* GetConnectionValue(size_t id, bool constant); + BaseValue* GetConnectionValue(const WorkflowNode::InputPin& inputPin); + void SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value); + void SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value); + + void ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin); + void ReportError(std::string message, const WorkflowNode& node); + void ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin); + void ReportWarning(std::string message, const WorkflowNode& node); + /// Run until all possible paths have been evaluated. + void Run(); }; diff --git a/core/src/Model/WorkflowNodes.cpp b/core/src/Model/WorkflowNodes.cpp new file mode 100644 index 0000000..8c315b6 --- /dev/null +++ b/core/src/Model/WorkflowNodes.cpp @@ -0,0 +1,309 @@ +#include "WorkflowNodes.hpp" + +#include "Utils/Macros.hpp" +#include "Utils/RTTI.hpp" +#include "Utils/Variant.hpp" + +#include <cassert> +#include <variant> + +WorkflowNode::Kind NumericOperationNode::OperationTypeToNodeKind(OperationType type) { + switch (type) { + case Addition: return KD_NumericAddition; + case Subtraction: return KD_NumericSubtraction; + case Multiplication: return KD_NumericMultiplication; + case Division: return KD_NumericDivision; + default: UNREACHABLE; + } +} + +NumericOperationNode::OperationType NumericOperationNode::NodeKindToOperationType(Kind kind) { + switch (kind) { + case KD_NumericAddition: return Addition; + case KD_NumericSubtraction: return Subtraction; + case KD_NumericMultiplication: return Multiplication; + case KD_NumericDivision: return Division; + default: UNREACHABLE; + } +} + +bool NumericOperationNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() >= KD_NumericAddition && node->GetKind() <= KD_NumericDivision; +} + +NumericOperationNode::NumericOperationNode(OperationType type) + : WorkflowNode(TransformType, OperationTypeToNodeKind(type)) + , mType{ type } { + mInputs.resize(2); + mInputs[0].MatchingType = BaseValue::KD_Numeric; + mInputs[1].MatchingType = BaseValue::KD_Numeric; + + mOutputs.resize(1); + mOutputs[0].MatchingType = BaseValue::KD_Numeric; +} + +void NumericOperationNode::Evaluate(WorkflowEvaluationContext& ctx) { + auto lhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[0])); + if (!lhsVal) return; + double lhs = lhsVal->GetValue(); + + auto rhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[1])); + if (!rhsVal) return; + double rhs = rhsVal->GetValue(); + + double res; + switch (mType) { + case Addition: res = lhs + rhs; break; + case Subtraction: res = lhs - rhs; break; + case Multiplication: res = lhs * rhs; break; + case Division: { + if (rhs == 0.0) { + // TODO localize + ctx.ReportError("Error: division by 0", *this); + return; + } + res = lhs / rhs; + } break; + + default: return; + } + + ctx.SetConnectionValue(mOutputs[0], res); +} + +bool NumericExpressionNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_NumericExpression; +} + +NumericExpressionNode::NumericExpressionNode() + : WorkflowNode(TransformType, KD_NumericExpression) { +} + +struct FormatTextNode::Argument { + ArgumentType ArgumentType; + int PinIdx; + + template <class TFunction> + static void ForArguments(std::vector<Element>::iterator begin, std::vector<Element>::iterator end, const TFunction& func) { + for (auto it = begin; it != end; ++it) { + auto& elm = *it; + if (auto arg = std::get_if<Argument>(&elm)) { + func(*arg); + } + } + } + + /// Find the pin index that the \c elmIdx -th element should have, based on the elements coming before it. + static int FindPinForElement(const std::vector<Element>& vec, int elmIdx) { + for (int i = elmIdx; i >= 0; --i) { + auto& elm = vec[i]; + if (auto arg = std::get_if<Argument>(&elm)) { + return arg->PinIdx + 1; + } + } + return 0; + } +}; + +BaseValue::Kind FormatTextNode::ArgumentTypeToValueKind(FormatTextNode::ArgumentType arg) { + switch (arg) { + case NumericArgument: return BaseValue::KD_Numeric; + case TextArgument: return BaseValue::KD_Text; + case DateTimeArgument: return BaseValue::KD_DateTime; + default: UNREACHABLE; + } +} + +bool FormatTextNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_TextFormatting; +} + +FormatTextNode::FormatTextNode() + : WorkflowNode(TransformType, KD_TextFormatting) { +} + +int FormatTextNode::GetElementCount() const { + return mElements.size(); +} + +const FormatTextNode::Element& FormatTextNode::GetElement(int idx) const { + return mElements[idx]; +} + +void FormatTextNode::SetElement(int idx, std::string text) { + assert(idx >= 0 && idx < mElements.size()); + + std::visit( + Overloaded{ + [&](const std::string& original) { mMinOutputChars -= original.size(); }, + [&](const Argument& original) { PreRemoveElement(idx); }, + }, + mElements[idx]); + + mMinOutputChars += text.size(); + mElements[idx] = std::move(text); +} + +void FormatTextNode::SetElement(int idx, ArgumentType argument) { + assert(idx >= 0 && idx < mElements.size()); + + std::visit( + Overloaded{ + [&](const std::string& original) { + mMinOutputChars -= original.size(); + + mElements[idx] = Argument{ + .ArgumentType = argument, + .PinIdx = Argument::FindPinForElement(mElements, idx), + }; + /* `original` is invalid from this point */ + }, + [&](const Argument& original) { + int pinIdx = original.PinIdx; + + // Create pin + auto& pin = mInputs[pinIdx]; + pin.MatchingType = ArgumentTypeToValueKind(argument); + + // Create element + mElements[idx] = Argument{ + .ArgumentType = argument, + .PinIdx = pinIdx, + }; + /* `original` is invalid from this point */ + }, + }, + mElements[idx]); +} + +void FormatTextNode::InsertElement(int idx, std::string text) { + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(std::move(text)); + + mMinOutputChars += text.size(); + mElements.insert(mElements.begin() + idx, std::move(text)); +} + +void FormatTextNode::InsertElement(int idx, ArgumentType argument) { + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(argument); + + int pinIdx = Argument::FindPinForElement(mElements, idx); + + // Create pin + auto& pin = InsertInputPin(pinIdx); + pin.MatchingType = ArgumentTypeToValueKind(argument); + + // Create element + mElements.insert( + mElements.begin() + idx, + Argument{ + .ArgumentType = argument, + .PinIdx = pinIdx, + }); +} + +void FormatTextNode::AppendElement(std::string text) { + mMinOutputChars += text.size(); + mElements.push_back(std::move(text)); +} + +void FormatTextNode::AppendElement(ArgumentType argument) { + int pinIdx = mInputs.size(); + // Create pin + mInputs.push_back(InputPin{}); + mInputs.back().MatchingType = ArgumentTypeToValueKind(argument); + // Creat eelement + mElements.push_back(Argument{ + .ArgumentType = argument, + .PinIdx = pinIdx, + }); +} + +void FormatTextNode::RemoveElement(int idx) { + assert(idx >= 0 && idx < mElements.size()); + + PreRemoveElement(idx); + if (auto arg = std::get_if<Argument>(&mElements[idx])) { + RemoveInputPin(arg->PinIdx); + } + mElements.erase(mElements.begin() + idx); +} + +void FormatTextNode::Evaluate(WorkflowEvaluationContext& ctx) { + std::string result; + result.reserve((size_t)(mMinOutputChars * 1.5f)); + + auto HandleText = [&](const std::string& str) { + result += str; + }; + auto HandleArgument = [&](const Argument& arg) { + switch (arg.ArgumentType) { + case NumericArgument: { + if (auto val = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetString(); + } else { + // TODO localize + ctx.ReportError("Non-numeric value connected to a numeric text format parameter.", *this); + } + } break; + case TextArgument: { + if (auto val = dyn_cast<TextValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetValue(); + } else { + // TODO localize + ctx.ReportError("Non-text value connected to a textual text format parameter.", *this); + } + } break; + case DateTimeArgument: { + if (auto val = dyn_cast<DateTimeValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetString(); + } else { + // TODO localize + ctx.ReportError("Non-date/time value connected to a date/time text format parameter.", *this); + } + } break; + } + }; + + for (auto& elm : mElements) { + std::visit(Overloaded{ HandleText, HandleArgument }, elm); + } +} + +void FormatTextNode::PreRemoveElement(int idx) { + auto& elm = mElements[idx]; + if (auto arg = std::get_if<Argument>(&elm)) { + RemoveInputPin(arg->PinIdx); + Argument::ForArguments( + mElements.begin() + idx + 1, + mElements.end(), + [&](Argument& arg) { + arg.PinIdx--; + }); + } +} + +bool DocumentTemplateExpansionNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_DocumentTemplateExpansion; +} + +DocumentTemplateExpansionNode::DocumentTemplateExpansionNode() + : WorkflowNode(TransformType, KD_DocumentTemplateExpansion) { +} + +bool FormInputNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_FormInput; +} + +FormInputNode::FormInputNode() + : WorkflowNode(InputType, KD_FormInput) { +} + +bool DatabaseRowsInputNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_DatabaseRowsInput; +} + +DatabaseRowsInputNode::DatabaseRowsInputNode() + : WorkflowNode(InputType, KD_DatabaseRowsInput) { +} diff --git a/core/src/Model/WorkflowNodes.hpp b/core/src/Model/WorkflowNodes.hpp new file mode 100644 index 0000000..e7d4bfc --- /dev/null +++ b/core/src/Model/WorkflowNodes.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "Model/Workflow.hpp" + +#include <cstddef> +#include <memory> +#include <variant> +#include <vector> + +class NumericOperationNode : public WorkflowNode { +public: + enum OperationType { + Addition, + Subtraction, + Multiplication, + Division, + + InvalidType, + TypeCount = InvalidType, + }; + +private: + OperationType mType; + +public: + static Kind OperationTypeToNodeKind(OperationType type); + static OperationType NodeKindToOperationType(Kind kind); + static bool IsInstance(const WorkflowNode* node); + NumericOperationNode(OperationType type); + + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; + +class NumericExpressionNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + NumericExpressionNode(); + + // TODO +}; + +class FormatTextNode : public WorkflowNode { +public: + enum ArgumentType { + NumericArgument, + TextArgument, + DateTimeArgument, + }; + +private: + struct Argument; + using Element = std::variant<std::string, Argument>; + + std::vector<Element> mElements; + int mMinOutputChars; + +public: + static BaseValue::Kind ArgumentTypeToValueKind(ArgumentType arg); + static bool IsInstance(const WorkflowNode* node); + FormatTextNode(); + + int GetElementCount() const; + const Element& GetElement(int idx) const; + + void SetElement(int idx, std::string text); + void SetElement(int idx, ArgumentType argument); + void InsertElement(int idx, std::string text); + void InsertElement(int idx, ArgumentType argument); + void AppendElement(std::string text); + void AppendElement(ArgumentType argument); + void RemoveElement(int idx); + + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; + +private: + void PreRemoveElement(int idx); +}; + +class DocumentTemplateExpansionNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + DocumentTemplateExpansionNode(); + + // TODO +}; + +class FormInputNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + FormInputNode(); + + // TODO +}; + +class DatabaseRowsInputNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + DatabaseRowsInputNode(); + + // TODO +}; diff --git a/core/src/Model/WorkflowSteps.cpp b/core/src/Model/WorkflowSteps.cpp deleted file mode 100644 index 815c5ca..0000000 --- a/core/src/Model/WorkflowSteps.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "WorkflowSteps.hpp" - -NumericOperationStep::NumericOperationStep(NumericOperationStep::Type type) - : mType{ type } { - mInputs.resize(2); - mInputs[0].MatchingType = BaseValue::NumericType; - mInputs[1].MatchingType = BaseValue::NumericType; - - mOutputs.resize(1); - mOutputs[0].MatchingType = BaseValue::NumericType; -} diff --git a/core/src/Model/WorkflowSteps.hpp b/core/src/Model/WorkflowSteps.hpp deleted file mode 100644 index 819ca63..0000000 --- a/core/src/Model/WorkflowSteps.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include "Model/Workflow.hpp" - -#include <memory> - -class NumericOperationStep : public WorkflowStep { -public: - enum Type { - Addition, - Subtraction, - Multiplication, - Division, - - InvalidType, - TypeCount = InvalidType, - }; - -private: - Type mType; - -public: - NumericOperationStep(Type type); -}; - -class NumericExpressionStep : public WorkflowStep { -public: -}; - -class FormatTextStep : public WorkflowStep { -public: -}; - -class FormInputStep : public WorkflowStep { -public: -}; - -class DatabaseRowsInputStep : public WorkflowStep { -public: -}; - -class DocumentTemplateExpansionStep : public WorkflowStep { -public: -}; diff --git a/core/src/Model/fwd.hpp b/core/src/Model/fwd.hpp index a2156aa..a7df889 100644 --- a/core/src/Model/fwd.hpp +++ b/core/src/Model/fwd.hpp @@ -32,14 +32,15 @@ class TransactionModel; // Workflow.hpp class WorkflowConnection; -class WorkflowStep; +class WorkflowNode; class Workflow; +class WorkflowEvaluationError; class WorkflowEvaluationContext; // WorkflowSteps.hpp -class NumericOperationStep; -class NumericExpressionStep; -class FormatTextStep; -class FormInputStep; -class DatabaseRowsInputStep; -class DocumentTemplateExpansionStep; +class NumericOperationNode; +class NumericExpressionNode; +class FormatTextNode; +class DocumentTemplateExpansionNode; +class FormInputNode; +class DatabaseRowsInputNode; diff --git a/core/src/Utils/Macros.hpp b/core/src/Utils/Macros.hpp index cb949b2..ab846f4 100644 --- a/core/src/Utils/Macros.hpp +++ b/core/src/Utils/Macros.hpp @@ -1,6 +1,12 @@ -#pragma once +#pragma once #define CONCAT_IMPL(a, b) a##b #define CONCAT(a, b) CONCAT_IMPL(a, b) #define UNIQUE_NAME(prefix) CONCAT(prefix, __LINE__) + +#if defined(_MSC_VER) +# define UNREACHABLE __assume(false) +#elif defined(__clang__) || defined(__GNUC__) +# define UUNREACHABLE __builtin_unreachable() +#endif diff --git a/core/src/Utils/RTTI.hpp b/core/src/Utils/RTTI.hpp new file mode 100644 index 0000000..bc0d289 --- /dev/null +++ b/core/src/Utils/RTTI.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include <cassert> + +template <class T, class TBase> +bool is_a(TBase* t) { + assert(t != nullptr); + return T::IsInstance(t); +} + +template <class T, class TBase> +bool is_a_nullable(TBase* t) { + if (t) { + return is_a<T, TBase>(t); + } else { + return false; + } +} + +template <class T, class TBase> +T* dyn_cast(TBase* t) { + assert(t != nullptr); + if (T::IsInstance(t)) { + return static_cast<T*>(t); + } else { + return nullptr; + } +} + +template <class T, class TBase> +const T* dyn_cast(const TBase* t) { + assert(t != nullptr); + if (T::IsInstance(t)) { + return static_cast<const T*>(t); + } else { + return nullptr; + } +} + +template <class T, class TBase> +T* dyn_cast_nullable(TBase* t) { + if (!t) return nullptr; + return dyn_cast<T, TBase>(t); +} diff --git a/core/src/Utils/Variant.hpp b/core/src/Utils/Variant.hpp new file mode 100644 index 0000000..7fdb2dc --- /dev/null +++ b/core/src/Utils/Variant.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <utility> +#include <variant> + +template <class... Ts> +struct Overloaded : Ts... { using Ts::operator()...; }; +template <class... Ts> +Overloaded(Ts...) -> Overloaded<Ts...>; + +template <class... Args> +struct VariantCastProxy { + std::variant<Args...> v; + + template <class... ToArgs> + operator std::variant<ToArgs...>() const { + return std::visit( + [](auto&& arg) -> std::variant<ToArgs...> { return arg; }, + v); + } +}; + +/// Use snake_case naming to align with `static_cast`, `dynamic_cast`, etc.. +template <class... Args> +auto variant_cast(std::variant<Args...> v) -> VariantCastProxy<Args...> { + return { std::move(v) }; +} |