pub mod bridges;
pub mod capabilities;
pub mod entry_types;
pub mod fn_declarations;
pub mod traits;
pub mod wasm;
pub mod zome;
use crate::{
dna::{
bridges::Bridge,
entry_types::EntryTypeDef,
fn_declarations::{FnDeclaration, TraitFns},
},
entry::entry_type::EntryType,
error::{DnaError, HcResult, HolochainError},
};
use holochain_persistence_api::cas::content::{AddressableContent, Content};
use holochain_json_api::{
error::{JsonError, JsonResult},
json::JsonString,
};
use entry::entry_type::AppEntryType;
use multihash;
use serde_json::{self, Value};
use std::{
collections::BTreeMap,
convert::TryFrom,
hash::{Hash, Hasher},
};
fn empty_object() -> Value {
json!({})
}
fn zero_uuid() -> String {
String::from("00000000-0000-0000-0000-000000000000")
}
#[derive(Serialize, Deserialize, Clone, Debug, DefaultJson)]
pub struct Dna {
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub version: String,
#[serde(default = "zero_uuid")]
pub uuid: String,
#[serde(default)]
pub dna_spec_version: String,
#[serde(default = "empty_object")]
pub properties: Value,
#[serde(default)]
pub zomes: BTreeMap<String, zome::Zome>,
}
impl AddressableContent for Dna {
fn content(&self) -> Content {
Content::from(self.to_owned())
}
fn try_from_content(content: &Content) -> JsonResult<Self> {
Ok(Dna::try_from(content.to_owned())?)
}
}
impl Eq for Dna {}
impl Default for Dna {
fn default() -> Self {
Dna {
name: String::new(),
description: String::new(),
version: String::new(),
uuid: zero_uuid(),
dna_spec_version: String::from("2.0"),
properties: empty_object(),
zomes: BTreeMap::new(),
}
}
}
impl Dna {
pub fn new() -> Self {
Default::default()
}
pub fn to_json_pretty(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn get_zome(&self, zome_name: &str) -> Result<&zome::Zome, DnaError> {
self.zomes
.get(zome_name)
.ok_or_else(|| DnaError::ZomeNotFound(format!("Zome '{}' not found", &zome_name,)))
}
pub fn get_trait<'a>(&'a self, zome: &'a zome::Zome, trait_name: &str) -> Option<&'a TraitFns> {
zome.traits.get(trait_name)
}
pub fn get_function_with_zome_name(
&self,
zome_name: &str,
fn_name: &str,
) -> Result<&FnDeclaration, DnaError> {
let zome = self.get_zome(zome_name)?;
zome.get_function(&fn_name).ok_or_else(|| {
DnaError::ZomeFunctionNotFound(format!(
"Zome function '{}' not found in Zome '{}'",
&fn_name, &zome_name
))
})
}
pub fn get_wasm_from_zome_name<T: Into<String>>(&self, zome_name: T) -> Option<&wasm::DnaWasm> {
let zome_name = zome_name.into();
self.get_zome(&zome_name).ok().map(|ref zome| &zome.code)
}
pub fn get_trait_fns_with_zome_name(
&self,
zome_name: &str,
trait_name: &str,
) -> Result<&TraitFns, DnaError> {
let zome = self.get_zome(zome_name)?;
self.get_trait(zome, &trait_name).ok_or_else(|| {
DnaError::TraitNotFound(format!(
"Trait '{}' not found in Zome '{}'",
&trait_name, &zome_name
))
})
}
pub fn get_zome_name_for_app_entry_type(
&self,
app_entry_type: &AppEntryType,
) -> Option<String> {
let entry_type_name = String::from(app_entry_type.to_owned());
assert!(EntryType::has_valid_app_name(&entry_type_name));
for (zome_name, zome) in &self.zomes {
for zome_entry_type_name in zome.entry_types.keys() {
if *zome_entry_type_name
== EntryType::App(AppEntryType::from(entry_type_name.to_string()))
{
return Some(zome_name.clone());
}
}
}
None
}
pub fn get_entry_type_def(&self, entry_type_name: &str) -> Option<&EntryTypeDef> {
assert!(EntryType::has_valid_app_name(entry_type_name));
for zome in self.zomes.values() {
for (zome_entry_type_name, entry_type_def) in &zome.entry_types {
if *zome_entry_type_name
== EntryType::App(AppEntryType::from(entry_type_name.to_string()))
{
return Some(entry_type_def);
}
}
}
None
}
pub fn multihash(&self) -> Result<Vec<u8>, HolochainError> {
let s = String::from(JsonString::from(self.to_owned()));
multihash::encode(multihash::Hash::SHA2256, &s.into_bytes())
.map_err(|error| HolochainError::ErrorGeneric(error.to_string()))
}
pub fn get_required_bridges(&self) -> Vec<Bridge> {
self.zomes
.values()
.map(|zome| zome.get_required_bridges())
.flatten()
.collect()
}
pub fn verify(&self) -> HcResult<()> {
let errors: Vec<HolochainError> = self
.zomes
.iter()
.map(|(zome_name, zome)| {
if zome.code.code.len() > 0 {
Ok(())
} else {
Err(HolochainError::ErrorGeneric(format!(
"Zome {} has no code!",
zome_name
)))
}
})
.filter_map(|r| r.err())
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(HolochainError::ErrorGeneric(format!(
"invalid DNA: {:?}",
errors
)))
}
}
}
impl Hash for Dna {
fn hash<H: Hasher>(&self, state: &mut H) {
let s = String::from(JsonString::from(self.to_owned()));
s.hash(state);
}
}
impl PartialEq for Dna {
fn eq(&self, other: &Dna) -> bool {
JsonString::from(self.to_owned()) == JsonString::from(other.to_owned())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
extern crate base64;
use crate::{
dna::{
bridges::{Bridge, BridgePresence, BridgeReference},
entry_types::EntryTypeDef,
fn_declarations::{FnDeclaration, FnParameter, Trait},
zome::tests::test_zome,
},
entry::entry_type::{AppEntryType, EntryType},
};
use holochain_json_api::json::JsonString;
use holochain_persistence_api::cas::content::Address;
use std::convert::TryFrom;
pub fn test_dna() -> Dna {
let fixture = String::from(
r#"{
"name": "test",
"description": "test",
"version": "test",
"uuid": "00000000-0000-0000-0000-000000000000",
"dna_spec_version": "2.0",
"properties": {
"test": "test"
},
"zomes": {
"test": {
"description": "test",
"config": {},
"entry_types": {
"test": {
"description": "test",
"sharing": "public",
"links_to": [
{
"target_type": "test",
"link_type": "test"
}
],
"linked_from": []
}
},
"traits": {
"hc_public": {
"functions": ["test"]
}
},
"fn_declarations": [
{
"name": "test",
"inputs": [],
"outputs": []
}
],
"code": {
"code": "AAECAw=="
},
"bridges": []
}
}
}"#,
);
Dna::try_from(JsonString::from_json(&fixture)).unwrap()
}
#[test]
fn test_dna_new() {
let dna = Dna::new();
assert_eq!(format!("{:?}",dna),"Dna { name: \"\", description: \"\", version: \"\", uuid: \"00000000-0000-0000-0000-000000000000\", dna_spec_version: \"2.0\", properties: Object({}), zomes: {} }")
}
#[test]
fn test_dna_to_json_pretty() {
let dna = Dna::new();
assert_eq!(format!("{:?}",dna.to_json_pretty()),"Ok(\"{\\n \\\"name\\\": \\\"\\\",\\n \\\"description\\\": \\\"\\\",\\n \\\"version\\\": \\\"\\\",\\n \\\"uuid\\\": \\\"00000000-0000-0000-0000-000000000000\\\",\\n \\\"dna_spec_version\\\": \\\"2.0\\\",\\n \\\"properties\\\": {},\\n \\\"zomes\\\": {}\\n}\")")
}
#[test]
fn test_dna_get_zome() {
let dna = test_dna();
let result = dna.get_zome("foo zome");
assert_eq!(
format!("{:?}", result),
"Err(ZomeNotFound(\"Zome \\\'foo zome\\\' not found\"))"
);
let zome = dna.get_zome("test").unwrap();
assert_eq!(zome.description, "test");
}
#[test]
fn test_dna_get_trait() {
let dna = test_dna();
let zome = dna.get_zome("test").unwrap();
let result = dna.get_trait(zome, "foo trait");
assert!(result.is_none());
let cap = dna.get_trait(zome, "hc_public").unwrap();
assert_eq!(format!("{:?}", cap), "TraitFns { functions: [\"test\"] }");
}
#[test]
fn test_dna_get_trait_with_zome_name() {
let dna = test_dna();
let result = dna.get_trait_fns_with_zome_name("foo zome", "foo trait");
assert_eq!(
format!("{:?}", result),
"Err(ZomeNotFound(\"Zome \\\'foo zome\\\' not found\"))"
);
let result = dna.get_trait_fns_with_zome_name("test", "foo trait");
assert_eq!(
format!("{:?}", result),
"Err(TraitNotFound(\"Trait \\\'foo trait\\\' not found in Zome \\\'test\\\'\"))"
);
let trait_fns = dna
.get_trait_fns_with_zome_name("test", "hc_public")
.unwrap();
assert_eq!(
format!("{:?}", trait_fns),
"TraitFns { functions: [\"test\"] }"
);
let trait_fns = dna
.get_trait_fns_with_zome_name("test", "hc_public")
.unwrap();
assert_eq!(
format!("{:?}", trait_fns),
"TraitFns { functions: [\"test\"] }"
);
}
#[test]
fn test_dna_get_function_with_zome_name() {
let dna = test_dna();
let result = dna.get_function_with_zome_name("foo zome", "foo fun");
assert_eq!(
format!("{:?}", result),
"Err(ZomeNotFound(\"Zome \\\'foo zome\\\' not found\"))"
);
let result = dna.get_function_with_zome_name("test", "foo fun");
assert_eq!(format!("{:?}",result),"Err(ZomeFunctionNotFound(\"Zome function \\\'foo fun\\\' not found in Zome \\\'test\\\'\"))");
let fun = dna.get_function_with_zome_name("test", "test").unwrap();
assert_eq!(
format!("{:?}", fun),
"FnDeclaration { name: \"test\", inputs: [], outputs: [] }"
);
}
#[test]
fn test_dna_verify() {
let dna = test_dna();
assert!(dna.verify().is_ok())
}
#[test]
fn test_dna_verify_fail() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"zomes": {
"my_zome": {
"code": {"code": ""}
}
}
}"#,
))
.unwrap();
assert!(dna.verify().is_err())
}
static UNIT_UUID: &'static str = "00000000-0000-0000-0000-000000000000";
fn test_empty_dna() -> Dna {
Dna::new()
}
#[test]
fn get_entry_type_def_test() {
let mut dna = test_empty_dna();
let mut zome = test_zome();
let entry_type = EntryType::App(AppEntryType::from("bar"));
let entry_type_def = EntryTypeDef::new();
zome.entry_types
.insert(entry_type.into(), entry_type_def.clone());
dna.zomes.insert("zome".to_string(), zome);
assert_eq!(None, dna.get_entry_type_def("foo"));
assert_eq!(Some(&entry_type_def), dna.get_entry_type_def("bar"));
}
#[test]
fn can_parse_and_output_json() {
let dna = test_empty_dna();
let serialized = serde_json::to_string(&dna).unwrap();
let deserialized: Dna = serde_json::from_str(&serialized).unwrap();
assert_eq!(String::from("2.0"), deserialized.dna_spec_version);
}
#[test]
fn can_parse_and_output_json_helpers() {
let dna = test_empty_dna();
let json_string = JsonString::from(dna);
let deserialized = Dna::try_from(json_string).unwrap();
assert_eq!(String::from("2.0"), deserialized.dna_spec_version);
}
#[test]
fn parse_and_serialize_compare() {
let fixture = String::from(
r#"{
"name": "test",
"description": "test",
"version": "test",
"uuid": "00000000-0000-0000-0000-000000000000",
"dna_spec_version": "2.0",
"properties": {
"test": "test"
},
"zomes": {
"test": {
"description": "test",
"config": {},
"entry_types": {
"test": {
"properties": "test",
"sharing": "public",
"links_to": [
{
"target_type": "test",
"link_type": "test"
}
],
"linked_from": []
}
},
"traits": {
"hc_public": {
"functions": ["test"]
}
},
"fn_declarations": [
{
"name": "test",
"inputs": [],
"outputs": []
}
],
"code": {
"code": "AAECAw=="
},
"bridges": []
}
}
}"#,
)
.replace(char::is_whitespace, "");
let dna = Dna::try_from(JsonString::from_json(&fixture.clone())).unwrap();
println!("{}", dna.to_json_pretty().unwrap());
let serialized = String::from(JsonString::from(dna)).replace(char::is_whitespace, "");
assert_eq!(fixture, serialized);
}
#[test]
fn default_value_test() {
let mut dna = Dna {
uuid: String::from(UNIT_UUID),
..Default::default()
};
let mut zome = zome::Zome::empty();
zome.entry_types
.insert("".into(), entry_types::EntryTypeDef::new());
dna.zomes.insert("".to_string(), zome);
let expected = JsonString::from(dna.clone());
println!("{:?}", expected);
let fixture = Dna::try_from(JsonString::from_json(
r#"{
"name": "",
"description": "",
"version": "",
"uuid": "00000000-0000-0000-0000-000000000000",
"dna_spec_version": "2.0",
"properties": {},
"zomes": {
"": {
"description": "",
"config": {},
"entry_types": {
"": {
"description": "",
"sharing": "public",
"links_to": [],
"linked_from": []
}
},
"traits": {},
"fn_declarations": [],
"code": {"code": ""}
}
}
}"#,
))
.unwrap();
assert_eq!(dna, fixture);
}
#[test]
fn parse_with_defaults_dna() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
}"#,
))
.unwrap();
assert!(dna.uuid.len() > 0);
}
#[test]
fn parse_with_defaults_entry_type() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"zomes": {
"zome1": {
"code": {
"code": ""
},
"entry_types": {
"type1": {}
}
}
}
}"#,
))
.unwrap();
assert_eq!(
dna.zomes
.get("zome1")
.unwrap()
.entry_types
.get(&"type1".into())
.unwrap()
.sharing,
entry_types::Sharing::Public
);
}
#[test]
fn parse_wasm() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"zomes": {
"zome1": {
"entry_types": {
"type1": {}
},
"code": {
"code": "AAECAw=="
}
}
}
}"#,
))
.unwrap();
assert_eq!(vec![0, 1, 2, 3], *dna.zomes.get("zome1").unwrap().code.code);
}
#[test]
#[should_panic]
fn parse_fail_if_bad_type_dna() {
Dna::try_from(JsonString::from_json(
r#"{
"name": 42
}"#,
))
.unwrap();
}
#[test]
#[should_panic]
fn parse_fail_if_bad_type_zome() {
Dna::try_from(JsonString::from_json(
r#"{
"zomes": {
"zome1": {
"description": 42
}
}
}"#,
))
.unwrap();
}
#[test]
#[should_panic]
fn parse_fail_if_bad_type_entry_type() {
Dna::try_from(JsonString::from_json(
r#"{
"zomes": {
"zome1": {
"entry_types": {
"test": {
"properties": 42
}
}
}
}
}"#,
))
.unwrap();
}
#[test]
fn parse_accepts_arbitrary_dna_properties() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"properties": {
"str": "hello",
"num": 3.14159,
"bool": true,
"null": null,
"arr": [1, 2],
"obj": {"a": 1, "b": 2}
}
}"#,
))
.unwrap();
let props = dna.properties.as_object().unwrap();
assert_eq!("hello", props.get("str").unwrap().as_str().unwrap());
assert_eq!(3.14159, props.get("num").unwrap().as_f64().unwrap());
assert_eq!(true, props.get("bool").unwrap().as_bool().unwrap());
assert!(props.get("null").unwrap().is_null());
assert_eq!(
1_i64,
props.get("arr").unwrap().as_array().unwrap()[0]
.as_i64()
.unwrap()
);
assert_eq!(
1_i64,
props
.get("obj")
.unwrap()
.as_object()
.unwrap()
.get("a")
.unwrap()
.as_i64()
.unwrap()
);
}
#[test]
fn get_wasm_from_zome_name() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"name": "test",
"description": "test",
"version": "test",
"uuid": "00000000-0000-0000-0000-000000000000",
"dna_spec_version": "2.0",
"properties": {
"test": "test"
},
"zomes": {
"test zome": {
"name": "test zome",
"description": "test",
"config": {},
"entry_types": {},
"traits": {
"hc_public": {
}
},
"fn_declarations": [],
"code": {
"code": "AAECAw=="
}
}
}
}"#,
))
.unwrap();
let wasm = dna.get_wasm_from_zome_name("test zome");
assert_eq!("AAECAw==", base64::encode(&*wasm.unwrap().code));
let fail = dna.get_wasm_from_zome_name("non existant zome");
assert_eq!(None, fail);
}
#[test]
fn test_get_zome_name_for_entry_type() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"name": "test",
"description": "test",
"version": "test",
"uuid": "00000000-0000-0000-0000-000000000000",
"dna_spec_version": "2.0",
"properties": {
"test": "test"
},
"zomes": {
"test zome": {
"name": "test zome",
"description": "test",
"config": {},
"traits": {
"hc_public": {
"functions": []
}
},
"fn_declarations": [],
"entry_types": {
"test type": {
"description": "",
"sharing": "public"
}
},
"code": {
"code": ""
}
}
}
}"#,
))
.unwrap();
assert_eq!(
dna.get_zome_name_for_app_entry_type(&AppEntryType::from("test type"))
.unwrap(),
"test zome".to_string()
);
assert!(dna
.get_zome_name_for_app_entry_type(&AppEntryType::from("non existant entry type"))
.is_none());
}
#[test]
fn test_get_required_bridges() {
let dna = Dna::try_from(JsonString::from_json(
r#"{
"name": "test",
"description": "test",
"version": "test",
"uuid": "00000000-0000-0000-0000-000000000000",
"dna_spec_version": "2.0",
"properties": {
"test": "test"
},
"zomes": {
"test zome": {
"name": "test zome",
"description": "test",
"config": {},
"traits": {
"hc_public": {
"functions": []
}
},
"fn_declarations": [],
"entry_types": {
"test type": {
"description": "",
"sharing": "public"
}
},
"code": {
"code": ""
},
"bridges": [
{
"presence": "required",
"handle": "DPKI",
"reference": {
"dna_address": "Qmabcdef1234567890"
}
},
{
"presence": "optional",
"handle": "Vault",
"reference": {
"traits": {
"persona_management": {
"functions": [
{
"name": "get_persona",
"inputs": [{"name": "domain", "type": "string"}],
"outputs": [{"name": "persona", "type": "json"}]
}
]
}
}
}
},
{
"presence": "required",
"handle": "HCHC",
"reference": {
"traits": {
"happ_directory": {
"functions": [
{
"name": "get_happs",
"inputs": [],
"outputs": [{"name": "happs", "type": "json"}]
}
]
}
}
}
}
]
}
}
}"#,
))
.unwrap();
assert_eq!(
dna.get_required_bridges(),
vec![
Bridge {
presence: BridgePresence::Required,
handle: String::from("DPKI"),
reference: BridgeReference::Address {
dna_address: Address::from("Qmabcdef1234567890"),
}
},
Bridge {
presence: BridgePresence::Required,
handle: String::from("HCHC"),
reference: BridgeReference::Trait {
traits: btreemap! {
String::from("happ_directory") => Trait {
functions: vec![
FnDeclaration {
name: String::from("get_happs"),
inputs: vec![],
outputs: vec![FnParameter{
name: String::from("happs"),
parameter_type: String::from("json"),
}],
}
]
}
}
},
},
]
);
}
}