Skip to content

Commit

Permalink
JS-2100 PlanTemplate is updatable
Browse files Browse the repository at this point in the history
  • Loading branch information
Zschimmer committed Dec 6, 2024
1 parent e3fd1c4 commit 67650e8
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ MissingReferencedItem = "$itemKey requires the missing $referencedItemPath"
ItemIsStillReferenced = "$itemPath is still referenced by $referencingItemKey $moreInfo"
UnknownOrder = "Unknown Order:$orderId"
OrderCannotAttachedToPlan = "Order:$orderId cannot be attached to a Plan due to the Order's state"
OrderWouldNotMatchChangedPlanTemplate = "Order:$orderId in $planId would no longer match the changed patttern of the PlanTemplate"
AgentReset = "Agent:$agentPath has been reset"
JsonSeqFileClosed = "JSON sequence from file '$file' has been closed"
HistoricSnapshotServiceBusy = "Webservice for historic snapshot is busy with another request - try again later"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ final case class ControllerStateExecutor private(
workflow <- controllerState.repo.pathTo(Workflow)(freshOrder.workflowPath)
preparedArguments <- workflow.orderParameterList.prepareOrderArguments(
freshOrder, controllerId, controllerState.keyToItem(JobResource), nowScope)
maybePlanId <- controllerState.minimumOrderToPlanId(freshOrder)
maybePlanId <- controllerState.evalOrderToPlanId(freshOrder)
innerBlock <- workflow.nestedWorkflow(freshOrder.innerBlock)
startPosition <- freshOrder.startPosition.traverse:
checkStartAndStopPositionAndInnerBlock(_, workflow, freshOrder.innerBlock)
Expand Down
13 changes: 12 additions & 1 deletion js7-data/shared/src/main/scala/js7/data/Problems.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import js7.data.item.VersionedEvent.VersionedItemAddedOrChanged
import js7.data.item.{InventoryItemKey, InventoryItemPath, VersionId, VersionedItemId, VersionedItemPath}
import js7.data.node.NodeId
import js7.data.order.OrderId
import js7.data.plan.PlanId
import js7.data.value.expression.Expression
import js7.data.value.expression.Expression.FunctionCall
import scala.collection.immutable.Map.{Map1, Map2, Map3}
Expand Down Expand Up @@ -48,10 +49,20 @@ object Problems:
def arguments: Map[String, String] = Map(
"orderId" -> orderId.string)

final case class OrderCannotAttachedToPlanProblem(orderId: OrderId) extends Problem.Coded:
trait KeyedEventProblem extends Problem.Coded:
def key: Any

final case class OrderCannotAttachedToPlanProblem(orderId: OrderId) extends KeyedEventProblem:
def key: OrderId = orderId
def arguments: Map[String, String] = Map1(
"orderId", orderId.string)

final case class OrderWouldNotMatchChangedPlanTemplateProblem(orderId: OrderId, planId: PlanId)
extends Problem.Coded:
def arguments: Map[String, String] = Map2(
"orderId", orderId.string,
"planId", planId.toString)

final case class MissingReferencedItemProblem(
itemKey: InventoryItemKey,
referencedItemKey: InventoryItemPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import js7.data.item.{BasicItemEvent, ClientAttachments, InventoryItem, Inventor
import js7.data.job.{JobResource, JobResourcePath}
import js7.data.lock.{Lock, LockPath, LockState}
import js7.data.node.{NodeId, NodeName}
import js7.data.order.OrderEvent.{OrderNoticesExpected, OrderTransferred}
import js7.data.order.OrderEvent.{OrderNoticesExpected, OrderPlanAttached, OrderTransferred}
import js7.data.order.{Order, OrderEvent, OrderId}
import js7.data.orderwatch.{FileWatch, OrderWatch, OrderWatchEvent, OrderWatchPath, OrderWatchState, OrderWatchStateHandler}
import js7.data.plan.{OrderPlan, Plan, PlanId, PlanKey, PlanTemplate, PlanTemplateId, PlanTemplateState}
Expand Down Expand Up @@ -359,6 +359,20 @@ extends SignedItemContainer,
override protected def applyOrderEvent(orderId: OrderId, event: OrderEvent)
: Checked[ControllerState] =
event match
case OrderPlanAttached(planId) =>
for
self <- super.applyOrderEvent(orderId, event)
// Move Order from GlobalPlan's to PlanId's PlanTemplateState
planTemplateState <- self.keyTo(PlanTemplateState).checked(planId.planTemplateId)
global <- self.keyTo(PlanTemplateState).checked(PlanTemplateId.Global)
self <- self.update(
addItemStates =
global.removeOrder(PlanKey.Global, orderId)
:: planTemplateState.addOrder(planId.planKey, orderId)
:: Nil)
yield
self

case event: OrderTransferred =>
super.applyOrderEvent(orderId, event).map: updated =>
updated.copy(
Expand Down Expand Up @@ -500,19 +514,6 @@ extends SignedItemContainer,
addItemStates = orderWatchStates,
removeUnsignedSimpleItems = remove)

protected def onOrderPlanAttached(orderId: OrderId, planId: PlanId): Checked[ControllerState] =
// Move Order from GlobalPlan's to PlanId's PlanTemplateState
for
planTemplateState <- keyTo(PlanTemplateState).checked(planId.planTemplateId)
global <- keyTo(PlanTemplateState).checked(PlanTemplateId.Global)
s <- update(
addItemStates =
global.removeOrder(PlanKey.Global, orderId) ::
planTemplateState.addOrder(planId.planKey, orderId) ::
Nil)
yield
s

protected def update_(
addOrders: Seq[Order[Order.State]] = Nil,
removeOrders: Seq[OrderId] = Nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import js7.base.problem.Checked.RichCheckedIterable
import js7.base.problem.{Checked, Problem}
import js7.base.utils.ScalaUtils.syntax.*
import js7.data.event.Event
import js7.data.order.OrderEvent.{OrderAddedX, OrderPlanAttached}
import js7.data.order.OrderEvent.OrderAddedX
import js7.data.order.{MinimumOrder, Order, OrderEvent, OrderId}
import js7.data.plan.{PlanId, PlanTemplate}
import js7.data.state.EventDrivenStateView
Expand All @@ -13,8 +13,6 @@ trait ControllerStateView[Self <: ControllerStateView[Self]]
extends EventDrivenStateView[Self, Event]:
this: Self =>

protected def onOrderPlanAttached(orderId: OrderId, planId: PlanId): Checked[Self]

override protected def applyOrderEvent(orderId: OrderId, event: OrderEvent): Checked[Self] =
event match
case orderAdded: OrderAddedX =>
Expand All @@ -23,21 +21,15 @@ extends EventDrivenStateView[Self, Event]:
update(addOrders =
Order.fromOrderAdded(addedOrderId, orderAdded) :: Nil)

case OrderPlanAttached(planId) =>
for
self <- super.applyOrderEvent(orderId, event)
self <- self.onOrderPlanAttached(orderId, planId)
yield
self

case _ =>
super.applyOrderEvent(orderId, event)

final def minimumOrderToPlanId(order: MinimumOrder): Checked[Option[PlanId]] =
val scope = toMinimumOrderScope(order)
keyToItem(PlanTemplate).values.flatMap:
final def evalOrderToPlanId(order: MinimumOrder): Checked[Option[PlanId]] =
val scope = toPlanOrderScope(order)
keyToItem(PlanTemplate).values.toVector.map:
_.evalOrderToPlanId(scope)
.combineProblems
.map(_.flatten)
.flatMap: planIds =>
planIds.length match
case 0 => Right(None)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package js7.data.controller

import cats.syntax.foldable.*
import cats.syntax.option.*
import cats.syntax.traverse.*
import js7.base.log.Logger
import js7.base.problem.Problems.DuplicateKey
import js7.base.problem.{Checked, Problem}
import js7.base.utils.CatsUtils.syntax.sequence
import js7.base.utils.ScalaUtils.syntax.{RichBoolean, RichEither, RichPartialFunction}
import js7.data.Problems.OrderWouldNotMatchChangedPlanTemplateProblem
import js7.data.agent.AgentPath
import js7.data.crypt.SignedItemVerifier
import js7.data.event.KeyedEvent.NoKey
Expand All @@ -17,7 +20,7 @@ import js7.data.item.VersionedEvent.{VersionedItemChanged, VersionedItemRemoved}
import js7.data.item.{BasicItemEvent, InventoryItem, InventoryItemEvent, InventoryItemPath, ItemRevision, SignableSimpleItem, SimpleItemPath, UnsignedSimpleItem, VersionedEvent, VersionedItemPath}
import js7.data.order.OrderEvent
import js7.data.order.OrderEvent.OrderPlanAttached
import js7.data.plan.PlanTemplateId
import js7.data.plan.{PlanTemplate, PlanTemplateId, PlanTemplateState}
import js7.data.workflow.{Workflow, WorkflowControl, WorkflowControlId, WorkflowId, WorkflowPath, WorkflowPathControl, WorkflowPathControlPath}
import scala.collection.View

Expand Down Expand Up @@ -184,11 +187,32 @@ object VerifiedUpdateItemsExecutor:
if controllerState.deletionMarkedItems.contains(item.key) then
Left(Problem.pure(s"${item.key} is marked as deleted and cannot be changed"))
else
Right:
item.match
case item: PlanTemplate =>
controllerState.keyTo(PlanTemplateState).checked(item.id)
.flatMap: planTemplateState =>
checkOrdersMatchStillItsPlan(controllerState, planTemplateState.copy(item = item))
case _ => Checked.unit
.map: _ =>
UnsignedSimpleItemChanged:
item.withRevision:
existing.itemRevision.fold(ItemRevision.Initial/*not expected*/)(_.next).some

def checkOrdersMatchStillItsPlan(
controllerState: ControllerState,
planTemplateState: PlanTemplateState)
: Checked[Unit] =
planTemplateState.orderIds
.map(controllerState.idToOrder.checked)
.map:
_.flatMap: order =>
planTemplateState.item.evalOrderToPlanId(controllerState.toPlanOrderScope(order))
.flatMap: planId =>
(order.maybePlanId == planId) !!
OrderWouldNotMatchChangedPlanTemplateProblem(order.id, order.planId)
.sequence
.map(_.combineAll)

def attachPlanlessOrdersPlanTemplates(
controllerState: ControllerState,
verifiedUpdateItems: VerifiedUpdateItems)
Expand All @@ -200,7 +224,7 @@ object VerifiedUpdateItemsExecutor:
.filter(_.maybePlanId.isEmpty)
.toVector
.traverse: order =>
controllerState.minimumOrderToPlanId(order).map: maybePlanId =>
controllerState.evalOrderToPlanId(order).map: maybePlanId =>
maybePlanId.map: planId =>
// The Order will check whether the event is applicable
order.id <-: OrderPlanAttached(planId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package js7.data.event

import js7.base.problem.{Checked, Problem}
import js7.base.utils.ScalaUtils.syntax.*
import js7.data.Problems.OrderCannotAttachedToPlanProblem
import js7.data.Problems.{KeyedEventProblem, OrderCannotAttachedToPlanProblem}
import js7.data.event.EventDrivenState.*
import scala.util.boundary

Expand Down Expand Up @@ -55,8 +55,9 @@ trait EventDrivenState[Self <: EventDrivenState[Self, E], E <: Event] extends Ba
case stamped: Stamped[KeyedEvent[?]] => stamped.value
case ke: KeyedEvent[?] => ke
prblm match
case OrderCannotAttachedToPlanProblem(orderId) if keyedEvent.key ==
orderId => prblm
case prblm: KeyedEventProblem if prblm.key == keyedEvent.key =>
prblm

case _ =>
prblm.withPrefix(s"Event '$keyedEventOrStamped' cannot be applied to '${companion.name}':")

Expand Down
16 changes: 7 additions & 9 deletions js7-data/shared/src/main/scala/js7/data/plan/PlanTemplate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,17 @@ extends UnsignedSimpleItem:

/** @param scope is expected to contain the Order Scope.
* @return None iff `orderToPlanKey` expression evaluates to MissingValue (no match). */
def evalOrderToPlanId(scope: Scope): Option[Checked[PlanId]] =
def evalOrderToPlanId(scope: Scope): Checked[Option[PlanId]] =
evalOrderToPlanKey(scope)
.map(_.map:
PlanId(id, _))
.map(_.map(PlanId(id, _)))

/** @param scope is expected to contain the Order Scope.
* @return None iff `orderToPlanKey` expression evaluates to MissingValue (no match). */
private def evalOrderToPlanKey(scope: Scope): Option[Checked[PlanKey]] =
orderToPlanKey.eval(using scope)
.traverse(_.missingToNone)
.map:
_.flatMap(_.toStringValueString)
.flatMap(PlanKey.checked)
private def evalOrderToPlanKey(scope: Scope): Checked[Option[PlanKey]] =
orderToPlanKey.eval(using scope).flatMap:
_.missingToNone.traverse:
_.toStringValueString.flatMap(PlanKey.checked)


def toInitialItemState: PlanTemplateState =
PlanTemplateState(this, toOrderPlan = Map.empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import scala.collection.immutable.Map.Map1
import scala.collection.{View, immutable}

final case class PlanTemplateState(
planTemplate: PlanTemplate,
item: PlanTemplate,
toOrderPlan: Map[PlanKey, OrderPlan])
extends UnsignedSimpleItemState:

protected type Self = PlanTemplateState

val companion: PlanTemplateState.type = PlanTemplateState

val item: PlanTemplate =
planTemplate
def planTemplate: PlanTemplate =
item

def path: PlanTemplateId =
planTemplate.id
Expand All @@ -36,16 +36,18 @@ extends UnsignedSimpleItemState:
Problem:
s"$id is in use by ${
usedPlans.toVector.sorted.map: plan =>
import plan.{orderIds, planId}
if orderIds.size == 1 then
s"${planId.planKey} with ${orderIds.head}"
if plan.orderIds.size == 1 then
s"${plan.planId.planKey} with ${plan.orderIds.head}"
else
s"${planId.planKey} with ${orderIds.size} orders"
s"${plan.planId.planKey} with ${plan.orderIds.size} orders"
.mkString(", ")
}"

def orderIds: View[OrderId] =
toOrderPlan.values.view.flatMap(_.orderIds)

def updateItem(item: PlanTemplate): Checked[PlanTemplateState] =
Left(Problem("Update of PlanTemplate is still not supported"))
Right(copy(item = item))

def addOrder(planKey: PlanKey, orderId: OrderId): PlanTemplateState =
addOrders(Map1(planKey, Set(orderId)))
Expand Down
9 changes: 2 additions & 7 deletions js7-data/shared/src/main/scala/js7/data/state/StateView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import js7.data.job.{JobKey, JobResource}
import js7.data.lock.{LockPath, LockState}
import js7.data.order.Order.{FailedInFork, IsFreshOrReady, Processing}
import js7.data.order.OrderEvent.{LockDemand, OrderNoticesExpected}
import js7.data.order.{MinimumOrder, Order, OrderDetails, OrderId}
import js7.data.order.{MinimumOrder, Order, OrderId}
import js7.data.plan.{PlanId, PlanTemplateId}
import js7.data.value.expression.Scope
import js7.data.value.expression.scopes.{JobResourceScope, NowScope, OrderScopes}
Expand Down Expand Up @@ -197,14 +197,9 @@ trait StateView extends ItemContainer:
.get(WorkflowPathControlPath(workflowPath))
.exists(_.item.suspended)

/** The same Scope over the Order's whole lifetime. */
final def toMinimumOrderScope(order: MinimumOrder): Scope =
def toPlanOrderScope(order: MinimumOrder): Scope =
OrderScopes.minimumOrderScope(order, controllerId)

/** The same Scope over the Order's whole lifetime. */
final def toMinimumOrderScope(orderId: OrderId, orderDetails: OrderDetails): Scope =
OrderScopes.minimumOrderScope(orderId, orderDetails, controllerId)

/** A pure (stable, repeatable) Scope. */
final def toOrderScope(order: Order[Order.State]): Checked[Scope] =
toOrderScopes(order).map(_.pureOrderScope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ final class PlannableBoardTest
locally:
val otherConsumingOrder = FreshOrder(otherConsumingOrderId, consumingWorkflow.path)
controller.addOrderBlocking(otherConsumingOrder)
assert(controllerState.minimumOrderToPlanId(otherConsumingOrder) == Right(Some(
assert(controllerState.evalOrderToPlanId(otherConsumingOrder) == Right(Some(
dailyPlan.id / day0)))
execCmd(DeleteOrdersWhenTerminated(Set(otherConsumingOrderId)))
eventWatch.awaitNext[OrderNoticesExpected](_.key == otherConsumingOrderId)
Expand Down Expand Up @@ -139,7 +139,7 @@ final class PlannableBoardTest
locally:
val consumingOrderId = OrderId("#2024w47#CONSUME")
val consumingOrder = FreshOrder(consumingOrderId, consumingWorkflow.path)
assert(controllerState.minimumOrderToPlanId(consumingOrder) ==
assert(controllerState.evalOrderToPlanId(consumingOrder) ==
Right(Some(weeklyPlan.id / "2024w47")))

controller.addOrderBlocking(consumingOrder)
Expand Down Expand Up @@ -180,11 +180,11 @@ final class PlannableBoardTest
eventWatch.resetLastWatchedEventId()
locally: // No Plan fits --> Global Plan //
val order = FreshOrder(OrderId("X-#2#"), WorkflowPath("WORKFLOW"))
assert(controllerState.minimumOrderToPlanId(order) == Right(None))
assert(controllerState.evalOrderToPlanId(order) == Right(None))

locally: // Two Plans fit, Order is rejected //
val order = FreshOrder(OrderId("AB-#1#"), postingWorkflow.path)
assert(controllerState.minimumOrderToPlanId(order) == Left(Problem:
assert(controllerState.evalOrderToPlanId(order) == Left(Problem:
"Order:AB-#1# fits 2 Plans: Plan:APlan/1, Plan:BPlan/1 — An Order must not fit multiple Plans"))

val checked = controller.api.addOrder(order).await(99.s)
Expand All @@ -193,11 +193,11 @@ final class PlannableBoardTest

locally: // APlan fits //
val order = FreshOrder(OrderId("A-#1#"), WorkflowPath("WORKFLOW"))
assert(controllerState.minimumOrderToPlanId(order) == Right(Some(aPlan.id / "1")))
assert(controllerState.evalOrderToPlanId(order) == Right(Some(aPlan.id / "1")))

locally: // BPlan fits //
val order = FreshOrder(OrderId("B-#2#"), WorkflowPath("WORKFLOW"))
assert(controllerState.minimumOrderToPlanId(order) == Right(Some(bPlan.id / "2")))
assert(controllerState.evalOrderToPlanId(order) == Right(Some(bPlan.id / "2")))

"Announced Notices" - {
// Each test will announce a Notice at bBoard
Expand Down
Loading

0 comments on commit 67650e8

Please sign in to comment.