aboutsummaryrefslogtreecommitdiff
path: root/core/src/Model/Workflow
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/Model/Workflow')
-rw-r--r--core/src/Model/Workflow/Evaluation.cpp183
-rw-r--r--core/src/Model/Workflow/Evaluation.hpp57
-rw-r--r--core/src/Model/Workflow/Nodes/DocumentNodes.cpp15
-rw-r--r--core/src/Model/Workflow/Nodes/DocumentNodes.hpp12
-rw-r--r--core/src/Model/Workflow/Nodes/NumericNodes.cpp86
-rw-r--r--core/src/Model/Workflow/Nodes/NumericNodes.hpp41
-rw-r--r--core/src/Model/Workflow/Nodes/TextNodes.cpp215
-rw-r--r--core/src/Model/Workflow/Nodes/TextNodes.hpp50
-rw-r--r--core/src/Model/Workflow/Nodes/UserInputNodes.cpp26
-rw-r--r--core/src/Model/Workflow/Nodes/UserInputNodes.hpp21
-rw-r--r--core/src/Model/Workflow/Nodes/fwd.hpp15
-rw-r--r--core/src/Model/Workflow/Value.cpp9
-rw-r--r--core/src/Model/Workflow/Value.hpp28
-rw-r--r--core/src/Model/Workflow/Values/BasicValues.cpp76
-rw-r--r--core/src/Model/Workflow/Values/BasicValues.hpp46
-rw-r--r--core/src/Model/Workflow/Values/fwd.hpp6
-rw-r--r--core/src/Model/Workflow/Workflow.cpp396
-rw-r--r--core/src/Model/Workflow/Workflow.hpp163
-rw-r--r--core/src/Model/Workflow/fwd.hpp14
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;