Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mediator

In-process messaging for Pudu: requests, notifications, streams, pipeline behaviors, processors, and exception handling

0.1.0Apache-2.01

InstallClose

OrderingScenarioTest.pudu

Pudu134 lines6.8 KB

GitHub ↗
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}3940/// The shop's kinds, made once.41fn 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}4445/// The rules an order must keep.46fn orderRules() -> Validator.Validator[PlaceOrder] {47  Validator.add(Validator.create(), Rule.build(NumberRule.greaterThan(Rule.ruleFor("quantity", |order: PlaceOrder| order.quantity), 0)))48}4950/// A checkout that only sends.51fn 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}5455/// Runs the suite.56fn 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