blob: e913ae155c56d3c7322333ea6ff9a41f6b5e7968 [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::OutputConsumer, story_graph::Module, utils},
derivative::*,
failure::Error,
fidl_fuchsia_modular::{EntityMarker, EntityProxy, EntityResolverProxy},
fuchsia_syslog::macros::*,
futures::future::LocalFutureObj,
serde_json,
std::collections::{HashMap, HashSet},
};
type EntityReference = String;
pub trait ContextReader {
fn get_reference(
&self,
story_id: &str,
module_id: &str,
parameter_name: &str,
) -> Option<&EntityReference>;
// Compose output_consumers given consumed entity reference
// story id of consumer and type of consumed entity.
fn get_output_consumers(
&self,
entity_reference: &str,
consumer_story_id: &str,
consume_type: &str,
) -> Vec<OutputConsumer>;
fn current<'a>(&'a self) -> Box<dyn Iterator<Item = &'a ContextEntity> + 'a>;
}
pub trait ContextWriter {
fn contribute<'a>(
&'a mut self,
story_id: &'a str,
module_id: &'a str,
parameter_name: &'a str,
reference: &'a str,
) -> LocalFutureObj<'a, Result<(), Error>>;
fn withdraw(&mut self, story_id: &str, module_id: &str, parameter_name: &str);
#[cfg(test)] // Will be used through the ContextManager
fn withdraw_all(&mut self, story_id: &str, module_id: &str);
fn clear_contributor(&mut self, contributor: &Contributor);
// Restore context store according to modules in restored story graph.
fn restore_from_story<'a>(
&'a mut self,
modules: &'a Vec<Module>,
story_id: &'a str,
) -> LocalFutureObj<'a, Result<(), Error>>;
}
pub struct StoryContextStore {
context_entities: HashMap<EntityReference, ContextEntity>,
contributor_to_refs: HashMap<Contributor, HashSet<EntityReference>>,
entity_resolver: EntityResolverProxy,
}
#[derive(Derivative)]
#[derivative(Debug, Eq, PartialEq)]
pub struct ContextEntity {
pub reference: EntityReference,
pub contributors: HashSet<Contributor>,
pub types: HashSet<String>,
#[derivative(PartialEq = "ignore")]
// Optional purely for testing purposes.
entity: Option<EntityProxy>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Contributor {
ModuleContributor { story_id: String, module_id: String, parameter_name: String },
// TODO(miguelfrde): Add group scoped contributors
}
impl Contributor {
pub fn module_new(story_id: &str, module_id: &str, parameter_name: &str) -> Self {
Contributor::ModuleContributor {
story_id: story_id.to_string(),
module_id: module_id.to_string(),
parameter_name: parameter_name.to_string(),
}
}
}
impl ContextEntity {
fn new(reference: &str, entity: EntityProxy) -> Self {
ContextEntity {
reference: reference.into(),
types: HashSet::new(),
contributors: HashSet::new(),
entity: Some(entity),
}
}
#[cfg(test)]
pub async fn from_entity(
entity: EntityProxy,
contributors: HashSet<Contributor>,
) -> Result<Self, Error> {
use std::iter::FromIterator;
let reference = entity.get_reference().await?;
let types = HashSet::from_iter(entity.get_types().await?.into_iter());
Ok(ContextEntity { reference, types, contributors, entity: Some(entity) })
}
#[cfg(test)]
pub fn new_test(
reference: &str,
types: HashSet<String>,
contributors: HashSet<Contributor>,
) -> Self {
ContextEntity { reference: reference.to_string(), types, contributors, entity: None }
}
pub async fn get_string_data<'a>(
&'a self,
path: Vec<&'a str>,
entity_type: &'a str,
) -> Option<String> {
if self.entity.is_none() {
return None;
}
let data = self.entity.as_ref().unwrap().get_data(entity_type).await.ok()?;
data.and_then(|buffer| {
utils::vmo_buffer_to_string(buffer)
.map_err(|e| {
fx_log_err!("Failed to fetch data for entity ref:{}", self.reference);
e
})
.ok()
})
.and_then(|data| serde_json::from_str::<serde_json::Value>(&data).ok())
.and_then(|json_data| {
let result =
path.iter().fold(Some(&json_data), |data, part| data.and_then(|d| d.get(part)));
// TODO: support numeric types as well.
result.and_then(|r| r.as_str()).map(|s| s.to_string())
})
}
pub fn add_contributor(&mut self, contributor: Contributor) {
self.contributors.insert(contributor);
}
fn merge_types(&mut self, types: Vec<String>) {
self.types.extend(types.into_iter())
}
fn remove_contributor(&mut self, contributor: &Contributor) {
self.contributors.remove(contributor);
}
fn get_contributors_by_story_id<'a>(
&'a self,
target_story_id: &'a str,
) -> Vec<&'a Contributor> {
self.contributors
.iter()
.filter(|Contributor::ModuleContributor { story_id, .. }| story_id == target_story_id)
.collect()
}
}
impl StoryContextStore {
pub fn new(entity_resolver: EntityResolverProxy) -> Self {
StoryContextStore {
context_entities: HashMap::new(),
contributor_to_refs: HashMap::new(),
entity_resolver,
}
}
}
impl ContextReader for StoryContextStore {
fn get_reference(
&self,
story_id: &str,
module_id: &str,
parameter_name: &str,
) -> Option<&EntityReference> {
let contributor = Contributor::module_new(story_id, module_id, parameter_name);
// A module contributor will only have one reference at a time.
self.contributor_to_refs.get(&contributor).and_then(|references| references.iter().next())
}
fn get_output_consumers(
&self,
entity_reference: &str,
consumer_story_id: &str,
consume_type: &str,
) -> Vec<OutputConsumer> {
self.context_entities
.get(entity_reference)
.map(|context_entity| {
context_entity
.get_contributors_by_story_id(consumer_story_id)
.iter()
.map(
|Contributor::ModuleContributor {
story_id: _,
module_id,
parameter_name,
}| {
OutputConsumer::new(
entity_reference,
module_id,
parameter_name,
consume_type,
)
},
)
.collect()
})
.unwrap_or(vec![])
}
/// Iterate over the context entities.
fn current<'a>(&'a self) -> Box<dyn Iterator<Item = &'a ContextEntity> + 'a> {
Box::new(self.context_entities.values())
}
}
impl ContextWriter for StoryContextStore {
fn contribute<'a>(
&'a mut self,
story_id: &'a str,
module_id: &'a str,
parameter_name: &'a str,
reference: &'a str,
) -> LocalFutureObj<'a, Result<(), Error>> {
LocalFutureObj::new(Box::new(async move {
let contributor = Contributor::module_new(story_id, module_id, parameter_name);
// Get the entity proxy client.
let (entity_proxy, server_end) = fidl::endpoints::create_proxy::<EntityMarker>()?;
self.entity_resolver.resolve_entity(reference, server_end)?;
let types = entity_proxy.get_types().await?;
// Remove previous contributions for this contributor.
self.clear_contributor(&contributor);
// Track new entity
let context_entity = self
.context_entities
.entry(reference.to_string())
.or_insert(ContextEntity::new(reference, entity_proxy));
context_entity.add_contributor(contributor.clone());
context_entity.merge_types(types);
// Keep track of contributor => references removal.
self.contributor_to_refs
.entry(contributor)
.or_insert(HashSet::new())
.insert(reference.to_string());
Ok(())
}))
}
fn withdraw(&mut self, story_id: &str, module_id: &str, parameter_name: &str) {
let contributor = Contributor::ModuleContributor {
story_id: story_id.to_string(),
module_id: module_id.to_string(),
parameter_name: parameter_name.to_string(),
};
self.clear_contributor(&contributor);
}
#[cfg(test)]
fn withdraw_all(&mut self, story_id: &str, module_id: &str) {
let contributors: Vec<Contributor> = self
.contributor_to_refs
.keys()
.filter(|c| match c {
Contributor::ModuleContributor { story_id: s, module_id: m, .. } => {
s == story_id && m == module_id
}
})
.cloned()
.collect();
for contributor in contributors {
self.clear_contributor(&contributor);
}
}
fn clear_contributor(&mut self, contributor: &Contributor) {
self.contributor_to_refs.remove_entry(contributor).map(|(c, references)| {
for reference in references {
match self.context_entities.get_mut(&reference) {
None => continue,
Some(context_entity) => {
context_entity.remove_contributor(&c);
if context_entity.contributors.is_empty() {
self.context_entities.remove(&reference);
}
}
}
}
});
}
fn restore_from_story<'a>(
&'a mut self,
modules: &'a Vec<Module>,
story_id: &'a str,
) -> LocalFutureObj<'a, Result<(), Error>> {
LocalFutureObj::new(Box::new(async move {
for module in modules.iter() {
for (output_name, module_output) in module.module_data.outputs.iter() {
self.contribute(
story_id,
&module.module_id,
output_name,
&module_output.entity_reference,
)
.await?;
}
}
Ok(())
}))
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
models::Intent,
story_graph::{Module, ModuleData},
testing::{FakeEntityData, FakeEntityResolver},
},
fidl_fuchsia_modular::{EntityResolverMarker, EntityResolverProxy},
fuchsia_async as fasync,
maplit::{hashmap, hashset},
};
#[fasync::run_singlethreaded(test)]
async fn contribute() -> Result<(), Error> {
let mut story_context_store = StoryContextStore::new(test_entity_resolver());
init_context_store(&mut story_context_store).await?;
// Verify context store state
let expected_context_entities = hashmap! {
"foo".to_string() => ContextEntity::new_test("foo",
hashset!("foo-type".to_string()),
hashset!(
Contributor::module_new("story1", "mod-a", "param-foo"),
Contributor::module_new("story2", "mod-b", "param-baz"),
)),
"bar".to_string() => ContextEntity::new_test("bar",
hashset!("bar-type".to_string()),
hashset!(
Contributor::module_new("story1", "mod-a", "param-bar")
)),
};
assert_eq!(story_context_store.context_entities, expected_context_entities);
// Contributing the same entity shouldn't have an effect.
story_context_store.contribute("story1", "mod-a", "param-foo", "foo").await?;
assert_eq!(story_context_store.context_entities, expected_context_entities);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn withdraw() -> Result<(), Error> {
let mut story_context_store = StoryContextStore::new(test_entity_resolver());
init_context_store(&mut story_context_store).await?;
// Remove a few of them
story_context_store.withdraw("story2", "mod-b", "param-baz");
story_context_store.withdraw("story1", "mod-a", "param-bar");
// Verify context store state
let expected_context_entities = hashmap! {
"foo".to_string() => ContextEntity::new_test(
"foo",
hashset!("foo-type".to_string()),
hashset!(Contributor::module_new("story1", "mod-a", "param-foo")),
),
};
assert_eq!(story_context_store.context_entities, expected_context_entities);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn withdraw_all() -> Result<(), Error> {
let mut story_context_store = StoryContextStore::new(test_entity_resolver());
init_context_store(&mut story_context_store).await?;
// Withdraw all context for one of the mods
story_context_store.withdraw_all("story1", "mod-a");
// Verify context store state
let expected_context_entities = hashmap! {
"foo".to_string() =>
ContextEntity::new_test(
"foo",
hashset!("foo-type".to_string()),
hashset!(Contributor::module_new("story2", "mod-b", "param-baz")),
),
};
assert_eq!(story_context_store.context_entities, expected_context_entities);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn get_string_data() -> Result<(), Error> {
let entity_resolver = test_entity_resolver();
let (entity_proxy, server_end) = fidl::endpoints::create_proxy::<EntityMarker>()?;
entity_resolver.resolve_entity("foo", server_end)?;
let context_entity = ContextEntity::from_entity(entity_proxy, hashset!()).await?;
// Verify successful case
assert_eq!(
context_entity.get_string_data(vec!["a", "b", "c"], "foo-type").await,
Some("1".to_string())
);
// Verify case with path that leads to an object, not a string node.
assert_eq!(context_entity.get_string_data(vec!["a", "b"], "foo-type").await, None);
// Verify case with unknown path
assert_eq!(context_entity.get_string_data(vec!["a", "x", "c"], "foo-type").await, None);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn restore_from_story() -> Result<(), Error> {
let mut story_context_store = StoryContextStore::new(test_entity_resolver());
init_context_store(&mut story_context_store).await?;
assert_eq!(story_context_store.context_entities.get("foo").unwrap().contributors.len(), 2);
let mut module_data = ModuleData::new(Intent::new());
module_data.update_output("param-foo", Some("foo".to_string()));
let module = Module::new("some-mod", module_data);
let modules = vec![module];
story_context_store.restore_from_story(&modules, "some-story").await?;
// Verify that a new contributor is added.
assert_eq!(story_context_store.context_entities.get("foo").unwrap().contributors.len(), 3);
Ok(())
}
fn test_entity_resolver() -> EntityResolverProxy {
let (entity_resolver_client, request_stream) =
fidl::endpoints::create_proxy_and_stream::<EntityResolverMarker>().unwrap();
let mut fake_entity_resolver = FakeEntityResolver::new();
fake_entity_resolver.register_entity(
"foo",
FakeEntityData::new(
vec!["foo-type".into()],
include_str!("../test_data/nested-field.json"),
),
);
fake_entity_resolver
.register_entity("bar", FakeEntityData::new(vec!["bar-type".into()], ""));
fake_entity_resolver.spawn(request_stream);
entity_resolver_client
}
async fn init_context_store(story_context_store: &mut StoryContextStore) -> Result<(), Error> {
story_context_store.contribute("story1", "mod-a", "param-foo", "foo").await?;
story_context_store.contribute("story2", "mod-b", "param-baz", "foo").await?;
story_context_store.contribute("story1", "mod-a", "param-bar", "bar").await?;
Ok(())
}
}