/ game / src / db / mod.rs
mod.rs
  1  use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands};
  2  use crate::models::world::scenes::{Scene, Stage, StageOrStub};
  3  use crate::models::{Content, ContentContainer, Entity, Insertable};
  4  use anyhow::Result;
  5  use arangors::document::options::InsertOptions;
  6  use arangors::graph::{EdgeDefinition, Graph};
  7  use arangors::transaction::{TransactionCollections, TransactionSettings};
  8  use arangors::uclient::reqwest::ReqwestClient;
  9  use arangors::{
 10      AqlQuery, ClientError, Collection, Database as ArangoDatabase, Document, GenericConnection,
 11  };
 12  use serde::{Deserialize, Serialize};
 13  use serde_json::value::to_value as to_json_value;
 14  use serde_json::Value as JsonValue;
 15  use std::collections::HashMap;
 16  
 17  mod queries;
 18  
 19  /// Type alias for how we're storing IDs in DB. WOuld prefer to have
 20  /// strong UUIDs in code and strings in DB.
 21  pub type Key = String;
 22  
 23  enum CollectionType {
 24      Document,
 25      Edge,
 26  }
 27  
 28  // Document Collections
 29  const CMD_COLLECTION: &'static str = "command_cache";
 30  const SCENE_COLLECTION: &'static str = "scenes";
 31  const REGION_COLLECTION: &'static str = "regions";
 32  const PEOPLE_COLLECTION: &'static str = "people";
 33  const ITEMS_COLLECTION: &'static str = "items";
 34  const PROPS_COLLECTION: &'static str = "props";
 35  const RACES_COLLECTION: &'static str = "races";
 36  const OCCUPATIONS_COLLECTION: &'static str = "occupations";
 37  
 38  // Edge collections
 39  const GAME_WORLD_EDGES: &'static str = "game_world";
 40  const PERSON_ATTRS: &'static str = "person_attributes";
 41  
 42  // Graphs
 43  const GAME_WORLD_GRAPH: &'static str = "world";
 44  
 45  const DOC_COLLECTIONS: &'static [&str] = &[
 46      CMD_COLLECTION,
 47      SCENE_COLLECTION,
 48      REGION_COLLECTION,
 49      PEOPLE_COLLECTION,
 50      ITEMS_COLLECTION,
 51      PROPS_COLLECTION,
 52      RACES_COLLECTION,
 53      OCCUPATIONS_COLLECTION,
 54  ];
 55  
 56  const EDGE_COLLECTIONS: &'static [&str] = &[GAME_WORLD_EDGES, PERSON_ATTRS];
 57  
 58  // Change if we decide to use a different HTTP client.
 59  type ArangoHttp = ReqwestClient;
 60  type ActiveDatabase = ArangoDatabase<ArangoHttp>;
 61  type ArangoResult<T> = std::result::Result<T, ClientError>;
 62  
 63  /// Generic edge that relates things back and forth, where the
 64  /// relation property determines what kind of relation we actually
 65  /// have.
 66  #[derive(Serialize, Deserialize, Debug)]
 67  struct Edge {
 68      _from: String,
 69      _to: String,
 70      relation: String,
 71  }
 72  
 73  /// Convert an Arango response for a single document, which may be
 74  /// missing, into an Option type. Bubble up any other errors.
 75  fn extract_document<T>(document: ArangoResult<Document<T>>) -> Result<Option<T>> {
 76      match document {
 77          Ok(doc) => Ok(Some(doc.document)),
 78          Err(db_err) => match db_err {
 79              ClientError::Arango(ref arr_err) => {
 80                  if arr_err.error_num() == 1202 {
 81                      Ok(None)
 82                  } else {
 83                      Err(db_err.into())
 84                  }
 85              }
 86              _ => Err(db_err.into()),
 87          },
 88      }
 89  }
 90  
 91  fn take_first<T>(mut vec: Vec<T>) -> Option<T> {
 92      if vec.get(0).is_none() {
 93          None
 94      } else {
 95          Some(vec.swap_remove(0))
 96      }
 97  }
 98  
 99  fn insert_opts() -> InsertOptions {
100      InsertOptions::builder()
101          .silent(false)
102          .return_new(true)
103          .build()
104  }
105  
106  fn is_scene_stub(value: &JsonValue) -> bool {
107      value
108          .as_object()
109          .and_then(|v| v.get("scene"))
110          .and_then(|scene| scene.get("isStub"))
111          .and_then(|is_stub| is_stub.as_bool())
112          .unwrap_or(false)
113  }
114  
115  async fn insert_single<T>(collection: &Collection<ArangoHttp>, value: &mut T) -> Result<()>
116  where
117      T: Insertable + Clone + Serialize,
118  {
119      let doc = to_json_value(&value)?;
120      let resp = collection.create_document(doc, insert_opts()).await?;
121  
122      let header = resp.header().unwrap();
123  
124      value.set_key(header._key.clone());
125      value.set_id(header._id.clone());
126  
127      Ok(())
128  }
129  
130  #[derive(Serialize, Deserialize)]
131  struct UpsertResponse {
132      pub _id: String,
133      pub _key: String,
134  }
135  
136  async fn upsert_scene(db: &ActiveDatabase, scene: &mut Scene) -> Result<()> {
137      let scene_json = serde_json::to_string(&scene)?;
138      let query = queries::UPSERT_SCENE.replace("<SCENE_JSON>", &scene_json);
139  
140      let aql = AqlQuery::builder()
141          .query(&query)
142          .bind_var("@scene_collection", SCENE_COLLECTION)
143          .bind_var("scene_key", to_json_value(&scene._key).unwrap())
144          .build();
145  
146      //db.aql_bind_vars::<JsonValue>(&query, vars).await?;
147      let resp = take_first(db.aql_query::<UpsertResponse>(aql).await?)
148          .expect("did not get upsert response");
149  
150      scene._id = Some(resp._id);
151      scene._key = Some(resp._key);
152  
153      Ok(())
154  }
155  
156  fn content_collection(content: &Content) -> &'static str {
157      match content {
158          Content::Scene(_) => SCENE_COLLECTION,
159          Content::SceneStub(_) => SCENE_COLLECTION,
160          Content::Person(_) => PEOPLE_COLLECTION,
161          Content::Item(_) => ITEMS_COLLECTION,
162      }
163  }
164  
165  pub struct Database {
166      conn: arangors::GenericConnection<ArangoHttp>,
167      world_name: String,
168  }
169  
170  impl Database {
171      pub async fn new(conn: GenericConnection<ArangoHttp>, world_name: &str) -> Result<Database> {
172          let db = Database {
173              conn,
174              world_name: world_name.to_string(),
175          };
176  
177          db.init().await?;
178          Ok(db)
179      }
180  
181      async fn init(&self) -> Result<()> {
182          let dbs = self.conn.accessible_databases().await?;
183  
184          if !dbs.contains_key(&self.world_name) {
185              self.conn.create_database(&self.world_name).await?;
186          }
187  
188          self.create_collections(CollectionType::Document, DOC_COLLECTIONS)
189              .await?;
190          self.create_collections(CollectionType::Edge, EDGE_COLLECTIONS)
191              .await?;
192  
193          self.create_graphs().await?;
194  
195          Ok(())
196      }
197  
198      async fn create_collections(&self, coll_type: CollectionType, names: &[&str]) -> Result<()> {
199          let db = self.db().await?;
200          let in_db = db.accessible_collections().await?;
201  
202          for name in names {
203              if in_db.iter().find(|info| info.name == *name).is_none() {
204                  match coll_type {
205                      CollectionType::Document => db.create_collection(&name).await?,
206                      CollectionType::Edge => db.create_edge_collection(&name).await?,
207                  };
208              }
209          }
210  
211          Ok(())
212      }
213  
214      async fn create_graphs(&self) -> Result<()> {
215          let db = self.db().await?;
216  
217          let in_db = db.graphs().await?.graphs;
218  
219          if in_db
220              .iter()
221              .find(|graph| graph.name == GAME_WORLD_GRAPH)
222              .is_none()
223          {
224              let edge_def = EdgeDefinition {
225                  collection: GAME_WORLD_EDGES.to_string(),
226                  from: vec![SCENE_COLLECTION.to_string()],
227                  to: vec![
228                      ITEMS_COLLECTION.to_string(),
229                      REGION_COLLECTION.to_string(),
230                      OCCUPATIONS_COLLECTION.to_string(),
231                      PEOPLE_COLLECTION.to_string(),
232                      PROPS_COLLECTION.to_string(),
233                      RACES_COLLECTION.to_string(),
234                  ],
235              };
236  
237              let world_graph = Graph::builder()
238                  .edge_definitions(vec![edge_def])
239                  .name(GAME_WORLD_GRAPH.to_string())
240                  .build();
241  
242              db.create_graph(world_graph, false).await?;
243          }
244  
245          Ok(())
246      }
247  
248      async fn db(&self) -> Result<ArangoDatabase<ArangoHttp>> {
249          let db = self.conn.db(&self.world_name).await?;
250          Ok(db)
251      }
252  
253      async fn collection(&self, name: &str) -> Result<Collection<ArangoHttp>> {
254          let coll = self.db().await?.collection(name).await?;
255          Ok(coll)
256      }
257  
258      pub async fn store_content(&self, container: &mut ContentContainer) -> Result<()> {
259          let txn_settings = TransactionSettings::builder()
260              .collections(
261                  TransactionCollections::builder()
262                      .write(vec![
263                          SCENE_COLLECTION.to_string(),
264                          PEOPLE_COLLECTION.to_string(),
265                          ITEMS_COLLECTION.to_string(),
266                          GAME_WORLD_EDGES.to_string(),
267                      ])
268                      .build(),
269              )
270              .build();
271  
272          let txn = self.db().await?.begin_transaction(txn_settings).await?;
273  
274          // First, all contained content must be inserted.
275          for relation in container.contained.as_mut_slice() {
276              let collection = content_collection(&relation.content);
277              self.store_single_content(collection, &mut relation.content)
278                  .await?;
279          }
280  
281          // Now insert the container/owner content + relations
282          let collection = content_collection(&container.owner);
283          self.store_single_content(collection, &mut container.owner)
284              .await?;
285          self.relate_content(&container).await?;
286  
287          txn.commit_transaction().await?;
288  
289          Ok(())
290      }
291  
292      async fn relate_content(&self, container: &ContentContainer) -> Result<()> {
293          let game_world = self.collection(GAME_WORLD_EDGES).await?;
294  
295          let owner_id = container
296              .owner
297              .id()
298              .expect("Did not get an ID from inserted object!");
299  
300          for relation in container.contained.as_slice() {
301              let content_id = relation
302                  .content
303                  .id()
304                  .expect("Did not get ID from inserted contained object!");
305  
306              let outbound = Edge {
307                  _from: owner_id.to_string(),
308                  _to: content_id.to_string(),
309                  relation: relation.outbound.clone(),
310              };
311  
312              let inbound = Edge {
313                  _from: content_id.to_string(),
314                  _to: owner_id.to_string(),
315                  relation: relation.inbound.clone(),
316              };
317  
318              game_world
319                  .create_document(outbound, InsertOptions::default())
320                  .await?;
321              game_world
322                  .create_document(inbound, InsertOptions::default())
323                  .await?;
324          }
325  
326          Ok(())
327      }
328  
329      pub async fn store_single_content(&self, coll_name: &str, content: &mut Content) -> Result<()> {
330          let collection = self.collection(coll_name).await?;
331  
332          match content {
333              //Content::Scene(ref mut scene) => insert_single(&collection, scene).await?,
334              Content::Scene(ref mut scene) => upsert_scene(&self.db().await?, scene).await?,
335              Content::SceneStub(ref mut stub) => insert_single(&collection, stub).await?,
336              Content::Person(ref mut person) => insert_single(&collection, person).await?,
337              Content::Item(ref mut item) => insert_single(&collection, item).await?,
338          };
339  
340          Ok(())
341      }
342  
343      pub async fn load_stage(&self, scene_key: &str) -> Result<Option<StageOrStub>> {
344          let mut vars = HashMap::new();
345          vars.insert("scene_key", to_json_value(&scene_key).unwrap());
346          vars.insert("@scene_collection", SCENE_COLLECTION.into());
347  
348          let db = self.db().await?;
349  
350          let res = db
351              .aql_bind_vars::<JsonValue>(queries::LOAD_STAGE, vars)
352              .await?;
353  
354          let maybe_stage = take_first(res);
355  
356          if let Some(stage) = maybe_stage {
357              let stage_or_stub = if is_scene_stub(&stage) {
358                  // The stub is embedded in the scene field of the result.
359                  StageOrStub::Stub(serde_json::from_value(
360                      stage.get("scene").cloned().unwrap(),
361                  )?)
362              } else {
363                  StageOrStub::Stage(serde_json::from_value(stage)?)
364              };
365  
366              Ok(Some(stage_or_stub))
367          } else {
368              Ok(None)
369          }
370      }
371  
372      pub async fn stage_exists(&self, scene_key: &str) -> Result<bool> {
373          let mut vars = HashMap::new();
374  
375          vars.insert("scene_key", to_json_value(&scene_key).unwrap());
376          vars.insert("@scene_collection", SCENE_COLLECTION.into());
377  
378          let db = self.db().await?;
379          let stage_count = db
380              .aql_bind_vars::<JsonValue>(queries::LOAD_STAGE, vars)
381              .await?
382              .len();
383  
384          Ok(stage_count > 0)
385      }
386  
387      pub async fn entity_exists(&self, entity_key: &str) -> Result<bool> {
388          let mut vars = HashMap::new();
389  
390          vars.insert("entity_key", to_json_value(entity_key).unwrap());
391  
392          let db = self.db().await?;
393          let entity_count = db
394              .aql_bind_vars::<JsonValue>(queries::LOAD_ENTITY, vars)
395              .await?
396              .len();
397  
398          Ok(entity_count > 0)
399      }
400  
401      pub async fn load_entity_in_scene(
402          &self,
403          scene_key: &str,
404          entity_key: &str,
405      ) -> Result<Option<Entity>> {
406          let aql = AqlQuery::builder()
407              .query(queries::LOAD_ENTITY_IN_SCENE)
408              .bind_var("@scene_collection", SCENE_COLLECTION)
409              .bind_var("scene_key", to_json_value(scene_key)?)
410              .bind_var("entity_key", to_json_value(entity_key)?)
411              .build();
412  
413          let results = self.db().await?.aql_query(aql).await?;
414          Ok(take_first(results))
415      }
416  
417      pub async fn load_entity(&self, entity_key: &str) -> Result<Option<Entity>> {
418          let aql = AqlQuery::builder()
419              .query(queries::LOAD_ENTITY)
420              .bind_var("entity_key", to_json_value(entity_key)?)
421              .build();
422  
423          let results = self.db().await?.aql_query(aql).await?;
424          Ok(take_first(results))
425      }
426  
427      pub async fn cache_command(
428          &self,
429          raw_cmd: &str,
430          scene: &Scene,
431          parsed_cmds: &ParsedCommands,
432      ) -> Result<()> {
433          let collection = self.collection(CMD_COLLECTION).await?;
434          let doc = CachedParsedCommand {
435              raw: raw_cmd.to_string(),
436              scene_key: scene._key.as_ref().cloned().expect("scene is missing key"),
437              commands: parsed_cmds.clone(),
438          };
439  
440          collection.create_document(doc, insert_opts()).await?;
441          Ok(())
442      }
443  
444      pub async fn load_cached_command(
445          &self,
446          raw_cmd: &str,
447          scene: &Scene,
448      ) -> Result<Option<CachedParsedCommand>> {
449          let scene_key = scene._key.as_deref();
450          let aql = AqlQuery::builder()
451              .query(queries::LOAD_CACHED_COMMAND)
452              .bind_var("@cache_collection", CMD_COLLECTION)
453              .bind_var("raw_cmd", to_json_value(raw_cmd)?)
454              .bind_var("scene_key", to_json_value(scene_key)?)
455              .build();
456  
457          let results = self.db().await?.aql_query(aql).await?;
458          Ok(take_first(results))
459      }
460  }