
Store.pudu
Pudu153 lines6.2 KB
1/** @Test.Support.Placeholder.Store.Module — the placeholder records, changed only under a lock */2module Support.Placeholder.Store34import Std.Json as Json5import Std.Math as Math6import Std.Option as Option7import Std.Result as Result8import Std.Sync as Sync9import Support.Placeholder.Data as Data1011/** @Test.Support.Placeholder.Store.Store — every resource's records and the lock guarding them */12export type Store = { lock: Sync.Mutex, records: Sync.Cell[Map[Str, Array[Json.Json]]] }1314/** @Test.Support.Placeholder.Store.Query — which records a listing answers, and in what order */15export type Query = { filters: Array[(Str, Str)], start: Int, limit: Option[Int], sort: Option[Str], descending: Bool }161718export fn seeded() -> Store { Store{lock: Sync.mutex(), records: Sync.cell(Data.seed())} }192021export fn knows(resource: Str) -> Bool { Data.RESOURCES.contains(resource) }22232425export fn list(store: &Store, resource: Str, query: &Query) -> (Array[Json.Json], Int) {26 let matching = all(store, resource).filter(|record: Json.Json| query.filters.filter(|pair: (Str, Str)| fieldText(&record, pair[0]) != Some(pair[1])).isEmpty())27 let ordered = match query.sort {28 case Some(field) => {29 let ascending = matching.sortBy(|left: Json.Json, right: Json.Json| sortKey(&left, field) < sortKey(&right, field))30 if query.descending { ascending.reverse() } else { ascending }31 }32 case None => matching33 }34 let rest = if query.start >= ordered.length() { [] } else { ordered.slice(Math.max(query.start, 0), ordered.length()) }35 let page = match query.limit {36 case Some(most) => if most < rest.length() { rest.slice(0, Math.max(most, 0)) } else { rest }37 case None => rest38 }39 (page, matching.length())40}414243export fn find(store: &Store, resource: Str, id: Int) -> Option[Json.Json] {44 match all(store, resource).filter(|record: Json.Json| idOf(&record) == id) {45 case [first, ..] => Some(first)46 case _ => None47 }48}495051export fn create(store: &Store, resource: Str, fields: &Json.Json) -> Json.Json {52 change(store, fn(records: Map[Str, Array[Json.Json]]) -> (Map[Str, Array[Json.Json]], Json.Json) {53 let held = Option.unwrapOr(records.get(resource), [])54 let next = held.reduce(|top: Int, record: Json.Json| if idOf(&record) > top { idOf(&record) } else { top }, 0) + 155 let made = Json.withField(fields, "id", Json.Number(next))56 (records.insert(resource, held.push(made)), made)57 })58}596061export fn replace(store: &Store, resource: Str, id: Int, fields: &Json.Json) -> Option[Json.Json] {62 rewrite(store, resource, id, |_old: Json.Json| Json.withField(fields, "id", Json.Number(id)))63}646566export fn patch(store: &Store, resource: Str, id: Int, fields: &Json.Json) -> Option[Json.Json] {67 let pairs = Option.unwrapOr(Json.asObject(fields), [])68 rewrite(store, resource, id, fn(old: Json.Json) -> Json.Json {69 var merged = old70 for (name, value) in pairs {71 if name != "id" { merged = Json.withField(&merged, name, value) }72 }73 merged74 })75}767778export fn remove(store: &Store, resource: Str, id: Int) -> Bool {79 change(store, fn(records: Map[Str, Array[Json.Json]]) -> (Map[Str, Array[Json.Json]], Bool) {80 let held = Option.unwrapOr(records.get(resource), [])81 let kept = held.filter(|record: Json.Json| idOf(&record) != id)82 (records.insert(resource, kept), kept.length() < held.length())83 })84}858687export fn count(store: &Store, resource: Str) -> Int { all(store, resource).length() }888990export fn idOf(record: &Json.Json) -> Int {91 match Json.field(record, "id") {92 case Some(value) => Option.unwrapOr(Json.asInt(&value), 0)93 case None => 094 }95}969798fn fieldText(record: &Json.Json, name: Str) -> Option[Str] {99 let value = Json.field(record, name) ?100 match Json.asText(&value) {101 case Some(text) => Some(text)102 case None => Some(Json.encode(&value))103 }104}105106107fn sortKey(record: &Json.Json, field: Str) -> Str {108 match Json.field(record, field) {109 case Some(value) => match Json.asInt(&value) {110 case Some(number) => "0" + show(number + 1000000000).drop(1)111 case None => "1" + Option.unwrapOr(fieldText(record, field), "")112 }113 case None => "2"114 }115}116117118fn all(store: &Store, resource: Str) -> Array[Json.Json] {119 Option.unwrapOr(Result.unwrapOr(Sync.get(&store.records), mapOf([])).get(resource), [])120}121122123fn change[R](store: &Store, step: fn(Map[Str, Array[Json.Json]]) -> (Map[Str, Array[Json.Json]], R)) -> R {124 let cell = store.records125 match Sync.withLock(&store.lock, fn() -> R {126 let (next, answer) = step(Result.unwrapOr(Sync.get(&cell), mapOf([])))127 let _stored = Sync.set(&cell, next)128 answer129 }) {130 case Ok(answer) => answer131 case Err(problem) => panic("the placeholder store could not be locked: " + show(problem))132 }133}134135136fn rewrite(store: &Store, resource: Str, id: Int, step: fn(Json.Json) -> Json.Json) -> Option[Json.Json] {137 change(store, fn(records: Map[Str, Array[Json.Json]]) -> (Map[Str, Array[Json.Json]], Option[Json.Json]) {138 let held = Option.unwrapOr(records.get(resource), [])139 var found: Option[Json.Json] = None140 var updated: Array[Json.Json] = []141 for record in held {142 if idOf(&record) == id {143 let next = step(record)144 found = Some(next)145 updated = updated.push(next)146 } else {147 updated = updated.push(record)148 }149 }150 (records.insert(resource, updated), found)151 })152}153