
OrderingScenarioTest.pudu
Pudu134 lines6.8 KB
1/** @Test.Integration.OrderingScenario.Suite — one shop wired through every part of the mediator */2module Integration.OrderingScenarioTest34import Std.Io as Io5import Std.Test as Test6import PuduLangLog.Configuration as Configuration7import PuduLangLog as Log8import PuduLangLog.Sinks.Memory as Memory9import PuduLangMediator.Behaviors.Logging as Logging10import PuduLangMediator.Behaviors.Validation as Validation11import PuduLangMediator.Context as Context12import PuduLangMediator.Mediator as Mediator13import PuduLangMediator as Messaging14import PuduLangMediator.Notification as Notification15import PuduLangMediator.Publishing as Publishing16import PuduLangMediator.Reader as Reader17import PuduLangMediator.Registration as Registration18import PuduLangMediator.Request as Request19import PuduLangMediator.Sender as Sender20import PuduLangMediator.Stream as Stream21import PuduLangMediator.Utils.Shared as Shared22import PuduLangValidator.Rule as Rule23import PuduLangValidator.Rules.Number as NumberRule24import PuduLangValidator.Validator as Validator2526/** @Test.Integration.OrderingScenario.PlaceOrder — a command that places an order */27type PlaceOrder = { item: Str, quantity: Int }2829/** @Test.Integration.OrderingScenario.Placed — the notification an order raises */30type Placed = { id: Int, item: Str }3132/** @Test.Integration.OrderingScenario.Shop — the shop's message kinds */33type Shop = {34 place: Request.Kind[PlaceOrder, Int, Str],35 find: Request.Kind[Int, Str, Str],36 placed: Notification.Kind[Placed, Str],37 history: Stream.Kind[(), Placed, Str]38}394041fn shop() -> Shop {42 Shop{place: Request.kind("orders.place"), find: Request.kind("orders.find"), placed: Notification.kind("orders.placed"), history: Stream.kind("orders.history")}43}444546fn orderRules() -> Validator.Validator[PlaceOrder] {47 Validator.add(Validator.create(), Rule.build(NumberRule.greaterThan(Rule.ruleFor("quantity", |order: PlaceOrder| order.quantity), 0)))48}495051fn checkout(sender: dynamic Sender.Sender, kinds: &Shop, item: Str, quantity: Int) -> Messaging.Outcome[Int, Str] {52 sender.send(&kinds.place, PlaceOrder{item: item, quantity: quantity}, Context.create())53}545556fn main() -> Int {57 let kinds = shop()58 let orders: Shared.Shared[Array[Placed]] = Shared.shared([])59 let emails: Shared.Shared[Array[Str]] = Shared.shared([])60 let stock: Shared.Shared[Int] = Shared.shared(10)61 let memory = Memory.create()62 let recorder = Logging.of(Configuration.create().minimumLevel(Log.Debug).writeTo(Memory.sink(&memory)).createLogger())63 let placed = kinds.placed64 let late: Shared.Shared[Option[Mediator.Mediator]] = Shared.shared(None)65 let built = Mediator.buildWith(&Mediator.Options{..Mediator.defaults(), publisher: Publishing.Parallel{}}, [66 Registration.openBehavior(recorder),67 Validation.register(&kinds.place, orderRules()),68 Registration.handler(&kinds.place, fn(order: PlaceOrder, context: Context.Context) -> Messaging.Outcome[Int, Str] {69 let left = Shared.change(&stock, fn(held: Int) -> (Int, Int) { if held >= order.quantity { (held - order.quantity, held - order.quantity) } else { (held, 0 - 1) } })70 if left < 0 { return Messaging.raise("out of stock") }71 let id = Shared.change(&orders, fn(held: Array[Placed]) -> (Array[Placed], Int) {72 let next = Placed{id: held.length() + 1, item: order.item}73 (held.push(next), next.id)74 })75 Ok(id)76 }),77 Registration.postProcessor(&kinds.place, fn(order: PlaceOrder, id: Int, context: Context.Context) -> Messaging.Outcome[(), Str] {78 match Shared.current(&late) {79 case Some(itself) => Mediator.publishWith(&itself, &placed, Placed{id: id, item: order.item}, context)80 case None => Messaging.refuse("the mediator is not built yet")81 }82 }),83 Registration.handler(&kinds.find, fn(id: Int, _context: Context.Context) -> Messaging.Outcome[Str, Str] {84 match Shared.current(&orders).filter(|order: Placed| order.id == id) {85 case [order] => Ok(order.item)86 case _ => Messaging.raise("no order " + show(id))87 }88 }),89 Registration.notificationHandler(&kinds.placed, "email", fn(order: Placed, _context: Context.Context) -> Messaging.Outcome[(), Str] {90 Shared.update(&emails, |held: Array[Str]| held.push("order " + show(order.id)))91 Ok(())92 }),93 Registration.streamHandler(&kinds.history, fn(_all: (), context: Context.Context, sink: Stream.Sink[Placed]) -> Messaging.Outcome[(), Str] {94 for order in Shared.current(&orders) {95 Context.check(&context) ?96 if !sink(order) { return Ok(()) }97 }98 Ok(())99 })100 ])101 let mediator = match built {102 case Ok(found) => found103 case Err(invalid) => panic(Mediator.explain(&invalid))104 }105 Shared.update(&late, |_none: Option[Mediator.Mediator]| Some(mediator))106 let sender: dynamic Sender.Sender = mediator107 let first = checkout(sender, &kinds, "tea", 2)108 let second = checkout(sender, &kinds, "cups", 3)109 let invalid = checkout(sender, &kinds, "spoons", 0)110 let tooMany = checkout(sender, &kinds, "pots", 50)111 let published = Mediator.publish(&mediator, &placed, Placed{id: 99, item: "manual"})112 let found = Mediator.send(&mediator, &kinds.find, 2)113 let answer = Mediator.dispatch(&mediator, Request.envelope(&kinds.find, 1))114 let history = Mediator.reader(&mediator, &kinds.history, (), &Context.create(), 1)115 let firstOrder = Reader.next(&history)116 Reader.close(&history)117 let checks = Test.suite("Integration.OrderingScenario", &[118 Test.equals("orders are placed with increasing ids", &(first, second), &(Ok(1), Ok(2))),119 Test.equals("an invalid order is refused before the handler", &(match invalid { case Err(failure) => Messaging.violations(&failure).map(|one: Messaging.Violation| one.property) case Ok(_) => [] }), &["quantity"]),120 Test.equals("an order beyond the stock fails in the handler", &tooMany, &Err(Messaging.Raised("out of stock"))),121 Test.equals("only valid orders took stock", &Shared.current(&stock), &5),122 Test.equals("every placed order and a published notification reach the handler", &(published, Shared.current(&emails)), &(Ok(()), ["order 1", "order 2", "order 99"])),123 Test.equals("a query finds an order", &found, &Ok("cups")),124 Test.equals("an untyped query is answered", &Request.answerOf(&kinds.find, &answer), &Some(Ok("tea"))),125 Test.equals("history is read one order at a time", &firstOrder, &Some(Ok(Placed{id: 1, item: "tea"}))),126 Test.equals("every request was logged once at its start", &Memory.withTemplate(&memory, Logging.STARTED).length(), &6)127 ])128 let ran = Test.run(&checks)129 for failure in Test.failuresOf(&ran) {130 let _reported = Io.writeErrorLine(failure)131 }132 Test.report(&ran)133}134