Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

Store.pudu

Pudu153 lines6.2 KB

GitHub ↗
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 }1617/// A store holding the seed records.18export fn seeded() -> Store { Store{lock: Sync.mutex(), records: Sync.cell(Data.seed())} }1920/// Whether a resource exists.21export fn knows(resource: Str) -> Bool { Data.RESOURCES.contains(resource) }2223/// The records of a resource matching every filter, sorted, from `start`, at most `limit`, and how24/// many matched in all.25export 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}4142/// The record of a resource with an id.43export 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}4950/// Stores a new record under the next id and answers it.51export 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}5960/// Replaces a record's fields, keeping its id, and answers it, or `None` when it is absent.61export 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}6465/// Merges fields into a record, keeping its id, and answers it, or `None` when it is absent.66export 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}7677/// Removes a record and answers whether it existed.78export 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}8586/// The number of records a resource holds.87export fn count(store: &Store, resource: Str) -> Int { all(store, resource).length() }8889/// The id of a record, or 0.90export 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}9697/// A field written as the text a query compares it with.98fn 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}105106/// The key a sort compares: numbers by value, everything else by its text, numbers first.107fn 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}116117/// Every record of a resource as it stands.118fn all(store: &Store, resource: Str) -> Array[Json.Json] {119  Option.unwrapOr(Result.unwrapOr(Sync.get(&store.records), mapOf([])).get(resource), [])120}121122/// Applies one change to the records under the lock and answers what it computed.123fn 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}134135/// Rewrites one record under the lock and answers it, or `None` when it is absent.136fn 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