diff options
Diffstat (limited to 'core/src/Model/Workflow')
19 files changed, 1459 insertions, 0 deletions
diff --git a/core/src/Model/Workflow/Evaluation.cpp b/core/src/Model/Workflow/Evaluation.cpp new file mode 100644 index 0000000..111d34e --- /dev/null +++ b/core/src/Model/Workflow/Evaluation.cpp @@ -0,0 +1,183 @@ +#include "Evaluation.hpp" + +#include <queue> + +namespace { +enum class EvaluationStatus { + Unevaluated, + Success, + Failed, +}; +} // namespace + +struct WorkflowEvaluationContext::RuntimeNode { + EvaluationStatus Status = EvaluationStatus::Unevaluated; +}; + +struct WorkflowEvaluationContext::RuntimeConnection { + std::unique_ptr<BaseValue> Value; + + bool IsAvailableValue() const { + return Value != nullptr; + } +}; + +WorkflowEvaluationContext::WorkflowEvaluationContext(Workflow& workflow) + : mWorkflow{ &workflow } { + mRuntimeNodes.resize(workflow.mNodes.size()); + mRuntimeConnections.resize(workflow.mConnections.size()); +} + +BaseValue* WorkflowEvaluationContext::GetConnectionValue(size_t id, bool constant) { + if (constant) { + return mWorkflow->GetConstantById(id); + } else { + return mRuntimeConnections[id].Value.get(); + } +} + +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) { + mRuntimeConnections[id].Value = std::move(value); +} + +void WorkflowEvaluationContext::SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value) { + if (outputPin.IsConnected()) { + SetConnectionValue(outputPin.Connection, std::move(value)); + } +} + +void WorkflowEvaluationContext::Run() { + std::queue<size_t> candidates; // Stores index to nodes + int evaluatedCount = 0; + int erroredCount = 0; + + // Evaluate all the input nodes first + for (size_t i = 0; i < mRuntimeNodes.size(); ++i) { + if (mWorkflow->mNodes[i]->GetType() == WorkflowNode::InputType) { + candidates.push(i); + } + } + + auto AddOutputsToCandidates = [&](size_t idx) { + auto& node = *mWorkflow->mNodes[idx]; + auto& rNode = mRuntimeNodes[idx]; + for (auto& pin : node.mOutputs) { + if (!pin.IsConnected()) continue; + // TODO support the other variant + if (pin.GetSupportedDirection() != WorkflowConnection::OneToMany) continue; + + auto& rConn = mRuntimeConnections[pin.Connection]; + auto& conn = mWorkflow->mConnections[pin.Connection]; + if (rConn.IsAvailableValue()) { + for (WorkflowConnection::ConnectionPoint& cp : conn.MultiConnections) { + if (rNode.Status != EvaluationStatus::Unevaluated) { + candidates.push(cp.Node); + } + } + } + } + }; + auto FindCandidates = [&]() { + for (size_t i = 0; i < mWorkflow->mNodes.size(); ++i) { + auto& node = mWorkflow->mNodes[i]; + auto& rNode = mRuntimeNodes[i]; + + if (rNode.Status != EvaluationStatus::Unevaluated) { + continue; + } + + for (auto& pin : node->mInputs) { + if (!pin.IsConnected()) continue; + + auto& rConn = mRuntimeConnections[pin.Connection]; + if (!rConn.IsAvailableValue()) { + goto skip; + } + } + + candidates.push(i); + + skip: + continue; + } + }; + + while (true) { + while (!candidates.empty()) { + auto idx = candidates.front(); + auto& node = *mWorkflow->mNodes[idx]; + auto& rNode = mRuntimeNodes[idx]; + candidates.pop(); + + int preEvalErrors = mErrors.size(); + node.Evaluate(*this); + if (preEvalErrors != mErrors.size()) { + erroredCount++; + } else { + evaluatedCount++; + AddOutputsToCandidates(idx); + } + } + + if (evaluatedCount + erroredCount >= mRuntimeNodes.size()) { + break; + } + + // Candidates empty, but there are still possibly-evaluable nodes + FindCandidates(); + } + + for (size_t i = 0; i < mRuntimeNodes.size(); ++i) { + if (mWorkflow->mNodes[i]->GetType() == WorkflowNode::OutputType) { + // TODO + } + } +} + +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin) { + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = pinId, + .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin, + .Type = WorkflowEvaluationError::Error, + }); +} + +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node) { + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = -1, + .PinType = WorkflowEvaluationError::NoPin, + .Type = WorkflowEvaluationError::Error, + }); +} + +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin) { + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = pinId, + .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin, + .Type = WorkflowEvaluationError::Warning, + }); +} + +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node) { + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = -1, + .PinType = WorkflowEvaluationError::NoPin, + .Type = WorkflowEvaluationError::Warning, + }); +} diff --git a/core/src/Model/Workflow/Evaluation.hpp b/core/src/Model/Workflow/Evaluation.hpp new file mode 100644 index 0000000..be2e862 --- /dev/null +++ b/core/src/Model/Workflow/Evaluation.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "Model/Workflow/Workflow.hpp" + +#include <cstddef> +#include <cstdint> +#include <string> +#include <vector> + +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> mRuntimeNodes; + std::vector<RuntimeConnection> mRuntimeConnections; + 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/Workflow/Nodes/DocumentNodes.cpp b/core/src/Model/Workflow/Nodes/DocumentNodes.cpp new file mode 100644 index 0000000..66d2eae --- /dev/null +++ b/core/src/Model/Workflow/Nodes/DocumentNodes.cpp @@ -0,0 +1,15 @@ +#include "DocumentNodes.hpp" + +#include "Model/Workflow/Evaluation.hpp" +#include "Model/Workflow/Values/BasicValues.hpp" + +bool DocumentTemplateExpansionNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_DocumentTemplateExpansion; +} + +DocumentTemplateExpansionNode::DocumentTemplateExpansionNode() + : WorkflowNode(TransformType, KD_DocumentTemplateExpansion) { +} + +void DocumentTemplateExpansionNode::Evaluate(WorkflowEvaluationContext& ctx) { +} diff --git a/core/src/Model/Workflow/Nodes/DocumentNodes.hpp b/core/src/Model/Workflow/Nodes/DocumentNodes.hpp new file mode 100644 index 0000000..3b775ec --- /dev/null +++ b/core/src/Model/Workflow/Nodes/DocumentNodes.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Model/Workflow/Workflow.hpp" + +class DocumentTemplateExpansionNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + DocumentTemplateExpansionNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; diff --git a/core/src/Model/Workflow/Nodes/NumericNodes.cpp b/core/src/Model/Workflow/Nodes/NumericNodes.cpp new file mode 100644 index 0000000..1722224 --- /dev/null +++ b/core/src/Model/Workflow/Nodes/NumericNodes.cpp @@ -0,0 +1,86 @@ +#include "NumericNodes.hpp" + +#include "Model/Workflow/Evaluation.hpp" +#include "Model/Workflow/Values/BasicValues.hpp" +#include "Utils/Macros.hpp" +#include "Utils/RTTI.hpp" + +#include <cassert> +#include <utility> + +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; + } + + auto value = std::make_unique<NumericValue>(); + value->SetValue(res); + ctx.SetConnectionValue(mOutputs[0], std::move(value)); +} + +bool NumericExpressionNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_NumericExpression; +} + +NumericExpressionNode::NumericExpressionNode() + : WorkflowNode(TransformType, KD_NumericExpression) { +} + +void NumericExpressionNode::Evaluate(WorkflowEvaluationContext& ctx) { +} diff --git a/core/src/Model/Workflow/Nodes/NumericNodes.hpp b/core/src/Model/Workflow/Nodes/NumericNodes.hpp new file mode 100644 index 0000000..32610f6 --- /dev/null +++ b/core/src/Model/Workflow/Nodes/NumericNodes.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "Model/Workflow/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 + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +};
\ No newline at end of file diff --git a/core/src/Model/Workflow/Nodes/TextNodes.cpp b/core/src/Model/Workflow/Nodes/TextNodes.cpp new file mode 100644 index 0000000..3852c66 --- /dev/null +++ b/core/src/Model/Workflow/Nodes/TextNodes.cpp @@ -0,0 +1,215 @@ +#include "TextNodes.hpp" + +#include "Model/Workflow/Evaluation.hpp" +#include "Model/Workflow/Values/BasicValues.hpp" +#include "Utils/Macros.hpp" +#include "Utils/RTTI.hpp" +#include "Utils/Variant.hpp" + +#include <cassert> +#include <utility> +#include <variant> +#include <vector> + +class TextFormatterNode::Impl { +public: + 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 TextFormatterNode::ArgumentTypeToValueKind(TextFormatterNode::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 TextFormatterNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_TextFormatting; +} + +TextFormatterNode::TextFormatterNode() + : WorkflowNode(TransformType, KD_TextFormatting) { +} + +int TextFormatterNode::GetElementCount() const { + return mElements.size(); +} + +const TextFormatterNode::Element& TextFormatterNode::GetElement(int idx) const { + return mElements[idx]; +} + +void TextFormatterNode::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 TextFormatterNode::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 = Impl::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 TextFormatterNode::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 TextFormatterNode::InsertElement(int idx, ArgumentType argument) { + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(argument); + + int pinIdx = Impl::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 TextFormatterNode::AppendElement(std::string text) { + mMinOutputChars += text.size(); + mElements.push_back(std::move(text)); +} + +void TextFormatterNode::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 TextFormatterNode::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 TextFormatterNode::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 TextFormatterNode::PreRemoveElement(int idx) { + auto& elm = mElements[idx]; + if (auto arg = std::get_if<Argument>(&elm)) { + RemoveInputPin(arg->PinIdx); + Impl::ForArguments( + mElements.begin() + idx + 1, + mElements.end(), + [&](Argument& arg) { + arg.PinIdx--; + }); + } +}
\ No newline at end of file diff --git a/core/src/Model/Workflow/Nodes/TextNodes.hpp b/core/src/Model/Workflow/Nodes/TextNodes.hpp new file mode 100644 index 0000000..278db32 --- /dev/null +++ b/core/src/Model/Workflow/Nodes/TextNodes.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "Model/Workflow/Workflow.hpp" + +#include <cstddef> +#include <memory> +#include <variant> +#include <vector> + +class TextFormatterNode : public WorkflowNode { +public: + enum ArgumentType { + NumericArgument, + TextArgument, + DateTimeArgument, + }; + +private: + class Impl; + + struct Argument { + ArgumentType ArgumentType; + int PinIdx; + }; + 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); + TextFormatterNode(); + + 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); +};
\ No newline at end of file diff --git a/core/src/Model/Workflow/Nodes/UserInputNodes.cpp b/core/src/Model/Workflow/Nodes/UserInputNodes.cpp new file mode 100644 index 0000000..f59226a --- /dev/null +++ b/core/src/Model/Workflow/Nodes/UserInputNodes.cpp @@ -0,0 +1,26 @@ +#include "UserInputNodes.hpp" + +#include "Model/Workflow/Evaluation.hpp" +#include "Model/Workflow/Values/BasicValues.hpp" + +bool FormInputNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_FormInput; +} + +FormInputNode::FormInputNode() + : WorkflowNode(InputType, KD_FormInput) { +} + +void FormInputNode::Evaluate(WorkflowEvaluationContext& ctx) { +} + +bool DatabaseRowsInputNode::IsInstance(const WorkflowNode* node) { + return node->GetKind() == KD_DatabaseRowsInput; +} + +DatabaseRowsInputNode::DatabaseRowsInputNode() + : WorkflowNode(InputType, KD_DatabaseRowsInput) { +} + +void DatabaseRowsInputNode::Evaluate(WorkflowEvaluationContext& ctx) { +} diff --git a/core/src/Model/Workflow/Nodes/UserInputNodes.hpp b/core/src/Model/Workflow/Nodes/UserInputNodes.hpp new file mode 100644 index 0000000..fe66cb4 --- /dev/null +++ b/core/src/Model/Workflow/Nodes/UserInputNodes.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "Model/Workflow/Workflow.hpp" + +class FormInputNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + FormInputNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; + +class DatabaseRowsInputNode : public WorkflowNode { +public: + static bool IsInstance(const WorkflowNode* node); + DatabaseRowsInputNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; diff --git a/core/src/Model/Workflow/Nodes/fwd.hpp b/core/src/Model/Workflow/Nodes/fwd.hpp new file mode 100644 index 0000000..4153825 --- /dev/null +++ b/core/src/Model/Workflow/Nodes/fwd.hpp @@ -0,0 +1,15 @@ +#pragma once + +// DocumentNodes.hpp +class DocumentTemplateExpansionNode; + +// InputNodes.hpp +class FormInputNode; +class DatabaseRowsInputNode; + +// NumericNodes.hpp +class NumericOperationNode; +class NumericExpressionNode; + +// TextNodes.hpp +class TextFormatterNode; diff --git a/core/src/Model/Workflow/Value.cpp b/core/src/Model/Workflow/Value.cpp new file mode 100644 index 0000000..7e5aabf --- /dev/null +++ b/core/src/Model/Workflow/Value.cpp @@ -0,0 +1,9 @@ +#include "Value.hpp" + +BaseValue::BaseValue(Kind kind) + : mKind{ kind } { +} + +BaseValue::Kind BaseValue::GetKind() const { + return mKind; +} diff --git a/core/src/Model/Workflow/Value.hpp b/core/src/Model/Workflow/Value.hpp new file mode 100644 index 0000000..eb99c14 --- /dev/null +++ b/core/src/Model/Workflow/Value.hpp @@ -0,0 +1,28 @@ +#pragma once + +class BaseValue { +public: + enum Kind { + KD_Numeric, + KD_Text, + KD_DateTime, + + /// An unspecified type, otherwise known as "any" in some contexts. + KindInvalid, + KindCount = KindInvalid, + }; + +private: + Kind mKind; + +public: + BaseValue(Kind kind); + virtual ~BaseValue() = default; + + BaseValue(const BaseValue&) = delete; + BaseValue& operator=(const BaseValue&) = delete; + BaseValue(BaseValue&&) = default; + BaseValue& operator=(BaseValue&&) = default; + + Kind GetKind() const; +}; diff --git a/core/src/Model/Workflow/Values/BasicValues.cpp b/core/src/Model/Workflow/Values/BasicValues.cpp new file mode 100644 index 0000000..fd70acd --- /dev/null +++ b/core/src/Model/Workflow/Values/BasicValues.cpp @@ -0,0 +1,76 @@ +#include "BasicValues.hpp" + +#include <charconv> + +bool NumericValue::IsInstance(const BaseValue* value) { + return value->GetKind() == KD_Numeric; +} + +NumericValue::NumericValue() + : 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 { + return static_cast<int64_t>(mValue); +} + +double NumericValue::GetValue() const { + return mValue; +} + +void NumericValue::SetValue(double value) { + mValue = value; +} + +bool TextValue::IsInstance(const BaseValue* value) { + return value->GetKind() == KD_Text; +} + +TextValue::TextValue() + : BaseValue(BaseValue::KD_Text) { +} + +const std::string& TextValue::GetValue() const { + return mValue; +} + +void TextValue::SetValue(const std::string& value) { + mValue = value; +} + +bool DateTimeValue::IsInstance(const BaseValue* value) { + return value->GetKind() == KD_DateTime; +} + +DateTimeValue::DateTimeValue() + : 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 { + return mValue; +} + +void DateTimeValue::SetValue(const std::chrono::time_point<std::chrono::system_clock>& value) { + mValue = value; +} diff --git a/core/src/Model/Workflow/Values/BasicValues.hpp b/core/src/Model/Workflow/Values/BasicValues.hpp new file mode 100644 index 0000000..a116c8c --- /dev/null +++ b/core/src/Model/Workflow/Values/BasicValues.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "Model/Workflow/Value.hpp" + +#include <chrono> +#include <cstdint> +#include <string> + +class NumericValue : public BaseValue { +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); +}; + +class TextValue : public BaseValue { +private: + std::string mValue; + +public: + static bool IsInstance(const BaseValue* value); + TextValue(); + + const std::string& GetValue() const; + void SetValue(const std::string& value); +}; + +class DateTimeValue : public BaseValue { +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/Values/fwd.hpp b/core/src/Model/Workflow/Values/fwd.hpp new file mode 100644 index 0000000..24f8119 --- /dev/null +++ b/core/src/Model/Workflow/Values/fwd.hpp @@ -0,0 +1,6 @@ +#pragma once + +// BasicValues.hpp +class NumericValue; +class TextValue; +class DateTimeValue; diff --git a/core/src/Model/Workflow/Workflow.cpp b/core/src/Model/Workflow/Workflow.cpp new file mode 100644 index 0000000..a32149e --- /dev/null +++ b/core/src/Model/Workflow/Workflow.cpp @@ -0,0 +1,396 @@ +#include "Workflow.hpp" + +#include <algorithm> +#include <cassert> +#include <queue> +#include <utility> + +WorkflowConnection::WorkflowConnection() + : MultiConnections{} + , SingleConnection{ WorkflowNode::kInvalidId, -1 } + , ConnectionDirection{ OneToMany } { +} + +bool WorkflowConnection::IsValid() const { + return SingleConnection.Node != WorkflowNode::kInvalidId; +} + +std::span<WorkflowConnection::ConnectionPoint> WorkflowConnection::GetSourcePoints() { + switch (ConnectionDirection) { + case ManyToOne: return MultiConnections; + case OneToMany: return { &SingleConnection, 1 }; + } +} + +std::span<const WorkflowConnection::ConnectionPoint> WorkflowConnection::GetSourcePoints() const { + switch (ConnectionDirection) { + case ManyToOne: return MultiConnections; + case OneToMany: return { &SingleConnection, 1 }; + } +} + +std::span<WorkflowConnection::ConnectionPoint> WorkflowConnection::GetDestinationPoints() { + switch (ConnectionDirection) { + case ManyToOne: return { &SingleConnection, 1 }; + case OneToMany: return MultiConnections; + } +} + +std::span<const WorkflowConnection::ConnectionPoint> WorkflowConnection::GetDestinationPoints() const { + switch (ConnectionDirection) { + case ManyToOne: return { &SingleConnection, 1 }; + case OneToMany: return MultiConnections; + } +} + +bool WorkflowNode::InputPin::IsConstantConnection() const { + return ConnectionToConst && IsConnected(); +} + +bool WorkflowNode::InputPin::IsConnected() const { + return Connection != WorkflowConnection::kInvalidId; +} + +BaseValue::Kind WorkflowNode::InputPin::GetMatchingType() const { + return MatchingType; +} + +WorkflowConnection::Direction WorkflowNode::InputPin::GetSupportedDirection() const { + return AllowsMultipleConnections ? WorkflowConnection::ManyToOne : WorkflowConnection::OneToMany; +} + +bool WorkflowNode::OutputPin::IsConnected() const { + return Connection != WorkflowConnection::kInvalidId; +} + +BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const { + return MatchingType; +} + +WorkflowConnection::Direction WorkflowNode::OutputPin::GetSupportedDirection() const { + return AllowsMultipleConnections ? WorkflowConnection::OneToMany : WorkflowConnection::ManyToOne; +} + +WorkflowNode::WorkflowNode(Type type, Kind kind) + : mType{ type } + , mKind{ kind } { +} + +size_t WorkflowNode::GetId() const { + return mId; +} + +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 WorkflowNode::DisconnectInput(int nodeId) { + mWorkflow->DisconnectByDestination(*this, nodeId); +} + +bool WorkflowNode::IsInputConnected(int nodeId) const { + return mInputs[nodeId].IsConnected(); +} + +void WorkflowNode::ConnectOutput(int nodeId, WorkflowNode& input, int inputNodeId) { + mWorkflow->Connect(input, inputNodeId, *this, nodeId); +} + +void WorkflowNode::DisconnectOutput(int nodeId) { + mWorkflow->DisconnectBySource(*this, nodeId); +} + +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) { + // Delay assignment to ConnectionPoint::Pin so that the pinB loop (if it happens to run and looks in the same Connection) doesn't match the point updated in the pinA loop + using Pt = WorkflowConnection::ConnectionPoint; + Pt* pointA = nullptr; + Pt* pointB = nullptr; + + if (pinA.IsConnected() && !pinA.IsConstantConnection()) { + auto pts = mWorkflow->GetConnectionById(pinA.Connection)->GetDestinationPoints(); + auto it = std::find(pts.begin(), pts.end(), Pt{ mId, a }); + pointA = it == pts.end() ? nullptr : &*it; + } + if (pinB.IsConnected() && !pinB.IsConstantConnection()) { + auto pts = mWorkflow->GetConnectionById(pinB.Connection)->GetDestinationPoints(); + auto it = std::find(pts.begin(), pts.end(), Pt{ mId, b }); + pointB = it == pts.end() ? nullptr : &*it; + } + + if (pointA) pointA->Pin = b; + if (pointB) pointB->Pin = 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) { + using Pt = WorkflowConnection::ConnectionPoint; + Pt* pointA = nullptr; + Pt* pointB = nullptr; + + if (pinA.IsConnected()) { + auto pts = mWorkflow->GetConnectionById(pinA.Connection)->GetSourcePoints(); + auto it = std::find(pts.begin(), pts.end(), Pt{ mId, a }); + pointA = it == pts.end() ? nullptr : &*it; + } + if (pinB.IsConnected()) { + auto pts = mWorkflow->GetConnectionById(pinB.Connection)->GetSourcePoints(); + auto it = std::find(pts.begin(), pts.end(), Pt{ mId, b }); + pointB = it == pts.end() ? nullptr : &*it; + } + + if (pointA) pointA->Pin = b; + if (pointB) pointB->Pin = a; + } + + std::swap(pinA, pinB); +} + +WorkflowConnection* Workflow::GetConnectionById(size_t id) { + return &mConnections[id]; +} + +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; +} + +bool Workflow::Connect(WorkflowNode& sourceNode, int sourcePin, WorkflowNode& destinationNode, int destinationPin) { + auto& src = sourceNode.mOutputs[sourcePin]; + auto& dst = destinationNode.mInputs[destinationPin]; + + // Equivalent to `if (o.GetSupportedDirection() != i.GetSupportedDirection()) return false;` + // Cannot connect two pins of different type + if (src.AllowsMultipleConnections == dst.AllowsMultipleConnections) return false; + // Would be same as `dst.GetSupportedDirection()` because we validated that they are the same above + auto direction = src.GetSupportedDirection(); + + // TODO report error to user? + if (src.GetMatchingType() != dst.GetMatchingType()) { + return false; + } + + using Pt = WorkflowConnection::ConnectionPoint; + Pt* srcConnPt; + Pt* dstConnPt; + size_t connId; + + switch (direction) { + case WorkflowConnection::ManyToOne: { + if (dst.IsConnected()) return false; + + WorkflowConnection* conn; + if (src.IsConnected()) { + conn = &mConnections[src.Connection]; + connId = src.Connection; + } else { + auto p = AllocWorkflowConnection(); + conn = &p.first; + connId = p.second; + } + + srcConnPt = &conn->MultiConnections.emplace_back(); + dstConnPt = &conn->SingleConnection; + } break; + + case WorkflowConnection::OneToMany: { + if (src.IsConnected()) return false; + + WorkflowConnection* conn; + if (dst.IsConnected()) { + conn = &mConnections[src.Connection]; + connId = src.Connection; + } else { + auto p = AllocWorkflowConnection(); + conn = &p.first; + connId = p.second; + } + + srcConnPt = &conn->SingleConnection; + dstConnPt = &conn->MultiConnections.emplace_back(); + } break; + } + + srcConnPt->Node = sourceNode.GetId(); + srcConnPt->Pin = sourcePin; + dstConnPt->Node = destinationNode.GetId(); + dstConnPt->Pin = destinationPin; + + src.Connection = connId; + dst.Connection = connId; + return true; +} + +bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, int sourcePin) { + auto& sp = sourceNode.mOutputs[sourcePin]; + if (!sp.IsConnected()) return false; + + auto& conn = mConnections[sp.Connection]; + + using Pt = WorkflowConnection::ConnectionPoint; + switch (sp.GetSupportedDirection()) { + case WorkflowConnection::ManyToOne: { + // Recessive pin, remove ConnectionPoint associated with this pin only + + auto& vec = conn.MultiConnections; + vec.erase(std::remove(vec.begin(), vec.end(), Pt{ sourceNode.GetId(), sourcePin }), vec.end()); + + sp.Connection = WorkflowConnection::kInvalidId; + } break; + + case WorkflowConnection::OneToMany: { + // Dominate pin, removes whole connection + + for (auto& pt : conn.MultiConnections) { + auto& node = *mNodes[pt.Node]; + node.mInputs[pt.Pin].Connection = WorkflowConnection::kInvalidId; + } + sp.Connection = WorkflowConnection::kInvalidId; + + conn = {}; + } break; + } + + return true; +} + +bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, int destinationPin) { + auto& dp = destinationNode.mInputs[destinationPin]; + if (!dp.IsConnected()) return false; + if (dp.IsConstantConnection()) { + dp.ConnectionToConst = false; + dp.Connection = WorkflowConnection::kInvalidId; + return true; + } + + auto& conn = mConnections[dp.Connection]; + + using Pt = WorkflowConnection::ConnectionPoint; + switch (dp.GetSupportedDirection()) { + case WorkflowConnection::ManyToOne: { + // Dominate pin, removes whole connection + + for (auto& pt : conn.MultiConnections) { + auto& node = *mNodes[pt.Node]; + node.mOutputs[pt.Pin].Connection = WorkflowConnection::kInvalidId; + } + dp.Connection = WorkflowConnection::kInvalidId; + + conn = {}; + } break; + + case WorkflowConnection::OneToMany: { + // Recessive pin, remove ConnectionPoint associated with this pin only + + auto& vec = conn.MultiConnections; + vec.erase(std::remove(vec.begin(), vec.end(), Pt{ destinationNode.GetId(), destinationPin }), vec.end()); + + dp.Connection = WorkflowConnection::kInvalidId; + } break; + } + + return true; +} + +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 }; +} diff --git a/core/src/Model/Workflow/Workflow.hpp b/core/src/Model/Workflow/Workflow.hpp new file mode 100644 index 0000000..a7b2c31 --- /dev/null +++ b/core/src/Model/Workflow/Workflow.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include "Value.hpp" +#include "cplt_fwd.hpp" + +#include <cstddef> +#include <cstdint> +#include <limits> +#include <memory> +#include <span> +#include <string> +#include <vector> + +class WorkflowConnection { +public: + static constexpr auto kInvalidId = std::numeric_limits<size_t>::max(); + + enum Direction { + ManyToOne, + OneToMany, + }; + + struct ConnectionPoint { + size_t Node; + int Pin; + + bool operator==(const ConnectionPoint&) const = default; + }; + + std::vector<ConnectionPoint> MultiConnections; + ConnectionPoint SingleConnection; + Direction ConnectionDirection; + +public: + WorkflowConnection(); + + bool IsValid() const; + std::span<ConnectionPoint> GetSourcePoints(); + std::span<const ConnectionPoint> GetSourcePoints() const; + std::span<ConnectionPoint> GetDestinationPoints(); + std::span<const ConnectionPoint> GetDestinationPoints() const; +}; + +class WorkflowNode { +public: + static constexpr auto kInvalidId = std::numeric_limits<size_t>::max(); + + 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 InputPin { + size_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::KindInvalid; + bool ConnectionToConst = false; + bool AllowsMultipleConnections = 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; + BaseValue::Kind GetMatchingType() const; + WorkflowConnection::Direction GetSupportedDirection() const; + }; + + struct OutputPin { + size_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::KindInvalid; + bool AllowsMultipleConnections = false; + + bool IsConnected() const; + BaseValue::Kind GetMatchingType() const; + WorkflowConnection::Direction GetSupportedDirection() const; + }; + + friend class Workflow; + friend class WorkflowEvaluationContext; + + Workflow* mWorkflow; + size_t mId; + std::vector<InputPin> mInputs; + std::vector<OutputPin> mOutputs; + Type mType; + Kind mKind; + +public: + WorkflowNode(Type type, Kind kind); + virtual ~WorkflowNode() = 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, WorkflowNode& output, int outputNodeId); + void DisconnectInput(int nodeId); + bool IsInputConnected(int nodeId) const; + + void ConnectOutput(int nodeId, WorkflowNode& input, int inputNodeId); + void DisconnectOutput(int nodeId); + bool IsOutputConnected(int nodeId) const; + + virtual void Evaluate(WorkflowEvaluationContext& ctx) = 0; + +protected: + 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<WorkflowNode>> mNodes; + std::vector<std::unique_ptr<BaseValue>> mConstants; + +public: + WorkflowConnection* GetConnectionById(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); + + bool Connect(WorkflowNode& sourceNode, int sourcePin, WorkflowNode& destinationNode, int destinationPin); + bool DisconnectBySource(WorkflowNode& sourceNode, int sourcePin); + bool DisconnectByDestination(WorkflowNode& destinationNode, int destinationPin); + +private: + std::pair<WorkflowConnection&, size_t> AllocWorkflowConnection(); + std::pair<std::unique_ptr<WorkflowNode>&, size_t> AllocWorkflowStep(); +}; diff --git a/core/src/Model/Workflow/fwd.hpp b/core/src/Model/Workflow/fwd.hpp new file mode 100644 index 0000000..2323a91 --- /dev/null +++ b/core/src/Model/Workflow/fwd.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "Model/Workflow/Nodes/fwd.hpp" +#include "Model/Workflow/Values/fwd.hpp" + +// Value.hpp +class BaseValue; + +// Workflow.hpp +class WorkflowConnection; +class WorkflowNode; +class Workflow; +class WorkflowEvaluationError; +class WorkflowEvaluationContext; |