blob: d29ca3d52a37a698e2868c5640d44f976d34982f [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::models::Intent,
maplit::{hashmap, hashset},
serde_derive::{Deserialize, Serialize},
std::{
collections::{HashMap, HashSet},
time::{SystemTime, UNIX_EPOCH},
},
};
type EntityReference = String;
type EntityType = String;
type ModuleId = String;
type OutputName = String;
#[cfg(test)]
type StoryId = String;
#[cfg(test)]
pub struct SessionGraph {
stories: HashMap<StoryId, StoryGraph>,
}
#[cfg(test)]
impl SessionGraph {
/// Creates a new empty session graph.
pub fn new() -> Self {
SessionGraph { stories: hashmap!() }
}
/// Creates a new story entry in the graph.
pub fn new_story(&mut self, story_id: impl Into<String>) {
self.stories.insert(story_id.into(), StoryGraph::new());
}
/// Returns the story graph for the given |story_id|.
pub fn get_story_graph(&self, story_id: &str) -> Option<&StoryGraph> {
self.stories.get(story_id)
}
/// Returns the mutable story graph for the given |story_id|.
pub fn get_story_graph_mut(&mut self, story_id: &str) -> Option<&mut StoryGraph> {
self.stories.get_mut(story_id)
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct StoryGraph {
modules: HashMap<ModuleId, ModuleData>,
}
impl StoryGraph {
/// Creates a new empty story graph.
pub fn new() -> Self {
StoryGraph { modules: hashmap!() }
}
/// Adds a module with the given initial intent to the graph.
pub fn add_module(&mut self, module_id: impl Into<String>, intent: Intent) {
let module_id_str = module_id.into();
self.modules.insert(module_id_str.clone(), ModuleData::new(intent));
}
/// Returns the module data associated to the given |module_id|.
pub fn get_module_data(&self, module_id: &str) -> Option<&ModuleData> {
self.modules.get(module_id)
}
/// Returns the mutable module data associated to the given |module_id|.
pub fn get_module_data_mut(&mut self, module_id: &str) -> Option<&mut ModuleData> {
self.modules.get_mut(module_id)
}
/// Returns an iterator of all modules in it.
pub fn get_all_modules(&self) -> impl Iterator<Item = (&ModuleId, &ModuleData)> {
self.modules.iter()
}
/// Retures the number of modules in this story.
#[cfg(test)]
pub fn get_module_count(&self) -> usize {
self.modules.len()
}
}
/// Holds both module_id and corresponding module_data.
pub struct Module {
pub module_id: String,
pub module_data: ModuleData,
}
impl Module {
pub fn new(module_id: impl Into<String>, module_data: ModuleData) -> Self {
Module { module_id: module_id.into(), module_data }
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct ModuleData {
pub outputs: HashMap<OutputName, ModuleOutput>,
children: HashSet<ModuleId>,
pub last_intent: Intent,
created_timestamp: u128,
last_modified_timestamp: u128,
}
impl ModuleData {
/// Creates a new empty module data with the given |intent| as the intial one.
pub fn new(intent: Intent) -> Self {
let timestamp =
SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_nanos();
ModuleData {
children: hashset!(),
outputs: hashmap!(),
last_intent: intent,
created_timestamp: timestamp,
last_modified_timestamp: timestamp,
}
}
/// Updates an output with the given reference. If no reference is given, the
/// output is removed.
pub fn update_output(&mut self, output_name: &str, new_reference: Option<String>) {
match new_reference {
Some(reference) => {
let output = self
.outputs
.entry(output_name.to_string())
.or_insert(ModuleOutput::new(reference.clone()));
output.update_reference(reference);
}
None => {
self.outputs.remove(output_name);
}
}
self.update_timestamp();
}
/// Add new consumer for an output.
pub fn add_output_consumer(
&mut self,
output_name: impl Into<String>,
reference: impl Into<String>,
module_id: impl Into<String>,
entity_type: impl Into<String>,
) {
let output =
self.outputs.entry(output_name.into()).or_insert(ModuleOutput::new(reference.into()));
output.add_consumer(module_id, entity_type);
self.update_timestamp();
}
/// Updates the last intent issued to the module with |new_intent|.
pub fn update_intent(&mut self, new_intent: Intent) {
self.last_intent = new_intent;
self.update_timestamp();
}
/// Links two mods through intents. This means this module issued an intent to
/// the module with id |child_module_id|.
pub fn add_child(&mut self, child_module_id: impl Into<String>) {
self.children.insert(child_module_id.into());
self.update_timestamp();
}
#[cfg(test)]
/// Unlinks two mods through linked through intent issuing.
pub fn remove_child(&mut self, child_module_id: &str) {
self.children.remove(child_module_id);
self.update_timestamp();
}
fn update_timestamp(&mut self) {
self.last_modified_timestamp =
SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_nanos();
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct ModuleOutput {
pub entity_reference: EntityReference,
pub consumers: HashSet<(ModuleId, EntityType)>,
}
impl ModuleOutput {
fn new(entity_reference: impl Into<String>) -> Self {
ModuleOutput { entity_reference: entity_reference.into(), consumers: hashset!() }
}
/// Links the mod outputing this output to the given mod with id |module_id|.
pub fn add_consumer(&mut self, module_id: impl Into<String>, entity_type: impl Into<String>) {
self.consumers.insert((module_id.into(), entity_type.into()));
}
/// Unlinks the mod outputing this output and the mod with id |module_id|.
#[cfg(test)]
pub fn remove_consumer(&mut self, module_id: &str) {
self.consumers.retain(|(m, _)| m != module_id);
}
fn update_reference(&mut self, new_reference: impl Into<String>) {
self.entity_reference = new_reference.into();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_graph() {
let mut session_graph = SessionGraph::new();
assert!(session_graph.stories.is_empty());
assert!(session_graph.get_story_graph("story_x").is_none());
assert!(session_graph.get_story_graph_mut("story_x").is_none());
assert_eq!(session_graph.stories.len(), 0);
session_graph.new_story("story_x");
assert!(session_graph.get_story_graph("story_x").is_some());
assert!(session_graph.get_story_graph_mut("story_x").is_some());
assert_eq!(session_graph.stories.len(), 1);
}
#[test]
fn story_graph() {
let mut story_graph = StoryGraph::new();
assert!(story_graph.modules.is_empty());
let intent = Intent::new().with_action("SOME_ACTION");
story_graph.add_module("mod-id", intent.clone());
assert_eq!(story_graph.get_module_count(), 1);
assert!(story_graph.modules.contains_key("mod-id"));
assert!(story_graph.get_module_data_mut("mod-id").is_some());
let module_data = story_graph.get_module_data("mod-id").unwrap();
assert_eq!(module_data.last_intent, intent);
}
#[test]
fn module_data() {
let intent = Intent::new().with_action("SOME_ACTION");
let mut module_data = ModuleData::new(intent.clone());
assert_eq!(module_data.last_intent, intent);
assert_eq!(module_data.created_timestamp, module_data.last_modified_timestamp);
assert!(module_data.children.is_empty());
assert!(module_data.outputs.is_empty());
let created_timestamp = module_data.created_timestamp;
let mut timestamps = vec![module_data.last_modified_timestamp];
// Verify intents
let new_intent = Intent::new().with_action("SOME_OTHER_ACTION");
module_data.update_intent(new_intent.clone());
assert_eq!(module_data.last_intent, new_intent);
timestamps.push(module_data.last_modified_timestamp);
// Verify children
module_data.add_child("other-mod");
assert!(module_data.children.contains("other-mod"));
timestamps.push(module_data.last_modified_timestamp);
module_data.remove_child("other-mod");
assert!(module_data.children.is_empty());
timestamps.push(module_data.last_modified_timestamp);
// Verify outputs
module_data.update_output("some-output", Some("some-ref".to_string()));
let output = module_data.outputs.get("some-output").unwrap();
assert_eq!(output.entity_reference, "some-ref");
assert!(output.consumers.is_empty());
timestamps.push(module_data.last_modified_timestamp);
module_data.add_output_consumer("some-output", "some-ref", "some_consumer_id", "some_type");
let new_output = module_data.outputs.get("some-output").unwrap();
assert!(!new_output.consumers.is_empty());
timestamps.push(module_data.last_modified_timestamp);
module_data.update_output("some-output", None);
assert!(module_data.outputs.is_empty());
timestamps.push(module_data.last_modified_timestamp);
// Verify all timestamp changes are incremental and created timestamp wasn't updated.
for i in 1..timestamps.len() {
assert!(timestamps[i] > timestamps[i - 1]);
}
assert_eq!(module_data.created_timestamp, created_timestamp);
}
#[test]
fn module_output() {
let mut module_output = ModuleOutput::new("some-ref");
assert!(module_output.consumers.is_empty());
assert_eq!(module_output.entity_reference, "some-ref");
module_output.add_consumer("some-consumer", "some-type");
module_output.add_consumer("other-consumer", "some-type");
assert_eq!(
module_output.consumers,
hashset!(
("some-consumer".to_string(), "some-type".to_string()),
("other-consumer".to_string(), "some-type".to_string())
)
);
module_output.remove_consumer("some-consumer");
assert_eq!(
module_output.consumers,
hashset!(("other-consumer".to_string(), "some-type".to_string()))
);
}
}