Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mock device extension #238

Merged
merged 110 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 108 commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
de7a738
chore: added extn folder
adamdama Aug 22, 2023
2d25e31
feat: added mockdevice contract
adamdama Aug 23, 2023
327599b
chore: renamed extn dir
adamdama Aug 23, 2023
817d22c
chore: added start of ffi
adamdama Aug 24, 2023
0dbf63c
chore: renamed contract
adamdama Aug 24, 2023
90910fc
tools: fix incorrect paths in setup script
adamdama Aug 24, 2023
c3c35c8
chore: mock_device extn loading
adamdama Aug 24, 2023
f54b63a
chore: added mock ws server
adamdama Aug 24, 2023
239db10
wip: mock server start
adamdama Aug 24, 2023
ddf0b37
feat: mock ws server listening for connections
adamdama Aug 24, 2023
5837db5
chore: added ability to modify mock_data at run time
adamdama Aug 24, 2023
2f1065a
Merge branch 'main' into mock-device
adamdama Sep 11, 2023
c145f59
feat: ripple boots from mock device extn
adamdama Sep 12, 2023
a9ef112
fix: added missing event payload to mock data
adamdama Sep 12, 2023
276ce1d
chore: cleared warnings
adamdama Sep 15, 2023
a4b8910
chore: mock web socket server stands up with shared state in extn
adamdama Sep 15, 2023
99dbc5f
feat: added rpc interface for mock device extn
adamdama Sep 18, 2023
0410b79
feat: mock device addRequestResponse working
adamdama Sep 19, 2023
9e92f67
feat: mock data can be removed from mock device
adamdama Sep 19, 2023
78de836
chore: removed Arc around mock_data mutex
adamdama Sep 19, 2023
a94c5af
Merge remote-tracking branch 'origin/main' into mock-device
adamdama Sep 19, 2023
27dded7
feat: added method to emit events
adamdama Sep 20, 2023
8b00eea
chore: mock_websocket_server from firebolt
adamdama Sep 20, 2023
359bf5b
chore: removed debug logs
adamdama Sep 20, 2023
540e033
refactor: added more flexible api surface
adamdama Sep 20, 2023
7b47724
chore: self-review todying
adamdama Sep 20, 2023
fec9d15
chore: fix manifest examples
adamdama Sep 20, 2023
c80adcd
refactor: better error handling and messages.
adamdama Sep 20, 2023
1ebc1ad
feat: mock server responding accross connections. dynamic jsonrpc id
adamdama Oct 4, 2023
fb797f5
refactor: added payload types so that we can explicitly support json-rpc
adamdama Oct 6, 2023
82b5beb
Merge remote-tracking branch 'origin/main' into mock-device
adamdama Oct 6, 2023
139ea9b
refactor: simplified names
adamdama Oct 6, 2023
dfc7660
chore: fixed clippy error
adamdama Oct 6, 2023
de65a36
chore: licenses
adamdama Oct 6, 2023
a7315fa
Merge branch 'main' into mock-device
adamdama Oct 9, 2023
3d1fd63
Merge branch 'main' into mock-device
adamdama Oct 9, 2023
6c9f4bc
test: unit tests for mock_data module
adamdama Oct 10, 2023
5de1289
refactor: removed message type in favour of payload type
adamdama Oct 11, 2023
1773a16
test: added mock websocket server tests
adamdama Oct 12, 2023
1ab77f5
chore: removed unneeded test
adamdama Oct 12, 2023
b1b5770
chore: added files for docs
adamdama Oct 12, 2023
6f444e3
refactor: use rpc_error util
adamdama Oct 13, 2023
c28f287
feat: mock data file location is now configurable
adamdama Oct 13, 2023
382fb38
refactor: fixed issue with contracts
adamdama Oct 13, 2023
33e9404
refactor: clean up errors. tried to add ffi test
adamdama Oct 16, 2023
8bd38e0
test: tried adding tests to mock device controller
adamdama Oct 16, 2023
02542a1
test: added placeholder test for processor
adamdama Oct 16, 2023
dca04ad
docs: added usage docs
adamdama Oct 16, 2023
bec114d
Merge branch 'main' into mock-device
adamdama Oct 16, 2023
465ddd6
chore: copyright notice
adamdama Oct 16, 2023
4df9a47
chore: missing copyright noticer
adamdama Oct 16, 2023
1a29df9
refactor: boot server panic message
adamdama Oct 16, 2023
179df32
merge: from main
satlead Nov 6, 2023
9fc9719
feat: Self contained extension provider (#301)
satlead Nov 7, 2023
3c8c723
chore: tidied use statements
adamdama Nov 8, 2023
9747bdc
test: fixed test for contract name change
adamdama Nov 8, 2023
208d47b
docs: updated mock device examples
adamdama Nov 8, 2023
8d19549
Merge branch 'main' into mock-device
adamdama Nov 9, 2023
b567f42
Merge branch 'main' into mock-device
adamdama Nov 13, 2023
78e2865
Add ADR for passthrough rpc
kpears201 Dec 11, 2023
b78a95b
Merge branch 'passthrough-rpc' of https://github.com/rdkcentral/Rippl…
satlead Jan 13, 2024
56efd6f
Merge branch 'main' of https://github.com/rdkcentral/Ripple into pass…
satlead Jan 29, 2024
03dc44c
feat: Communication Broker
satlead Jan 29, 2024
2c6ff7e
fix: atomicity and gateway
satlead Jan 30, 2024
0ded01e
fix: Http Broker
satlead Jan 31, 2024
da0e346
Merge branch 'main' of github.com:rdkcentral/Ripple into mock-device
satlead Jan 31, 2024
cb79e33
Merge branch 'passthru-impl' into passthru_with_mock
satlead Jan 31, 2024
4a333dd
fix: build errors
satlead Jan 31, 2024
61b610e
fix: clippy
satlead Jan 31, 2024
73662fb
feat: Working one
satlead Jan 31, 2024
754dc11
fix: Http broker
satlead Jan 31, 2024
1266109
fix: errors
satlead Feb 1, 2024
ecf6383
fix: PR Ready
satlead Feb 1, 2024
cb4d38d
fix: changes to the open rpc to remove user interest
satlead Feb 7, 2024
d0a1efe
fix: for making mock device rpc work
satlead Feb 7, 2024
82e6b57
fix: cleanup Mock data hash
satlead Feb 9, 2024
f11bf55
fix: Adding delay
satlead Feb 9, 2024
1395bc8
fix: Eventing mechanism
satlead Feb 14, 2024
ffbe320
Merge branch 'main' of github.com:rdkcentral/Ripple into passthru_wit…
satlead Feb 14, 2024
a1ee3fb
fix: Changes for eventing
satlead Feb 15, 2024
cd8e3bb
Merge branch 'main' of github.com:rdkcentral/Ripple into passthru_wit…
satlead Feb 16, 2024
7e9bb44
fix: cleanup pass through from mock
satlead Feb 16, 2024
738ba5f
fix: cleanup dependencies
satlead Feb 16, 2024
08bdab5
Merge branch 'main' of github.com:rdkcentral/Ripple into mock-device
satlead Feb 26, 2024
67c5287
fix: Dependecies
satlead Feb 26, 2024
8d19ac5
fix: Update unit tests
satlead Feb 27, 2024
8e82bca
Merge branch 'main' of github.com:rdkcentral/Ripple into mock-device
satlead Mar 4, 2024
46ba53a
Merge branch 'main' of github.com:rdkcentral/Ripple into mock-device
satlead Mar 5, 2024
85279aa
fix: clippy errors
satlead Mar 5, 2024
e017f13
fix: logger initialization issues
satlead Mar 5, 2024
060b6cf
Merge branch 'main' of https://github.com/rdkcentral/Ripple into mock…
satlead Mar 13, 2024
0b63e1d
fix: unit tests
satlead Mar 13, 2024
e9c2bc6
fix: logging error
satlead Mar 13, 2024
6e268f4
Merge branch 'main' of https://github.com/rdkcentral/Ripple into mock…
satlead Mar 14, 2024
e4a9a99
fix: Add emit support
satlead Mar 14, 2024
9db44ae
fix: unwrap
satlead Mar 14, 2024
fb5bb0a
fix: remove PartialEq impl
satlead Mar 15, 2024
55a12af
merge: from main
satlead Mar 15, 2024
02b7d50
fix: dependency errors
satlead Mar 15, 2024
b52e6dc
fix: cleanup manifests
satlead Mar 15, 2024
77e0792
Merge branch 'main' of https://github.com/rdkcentral/Ripple into mock…
satlead Mar 18, 2024
96d0436
fix: Add and remove requests
satlead Mar 20, 2024
a598ed0
Update docs
satlead Mar 20, 2024
5e06b90
fix: remove requests
satlead Mar 20, 2024
8114126
fix: for events
satlead Mar 20, 2024
0844761
Merge branch 'main' of https://github.com/rdkcentral/Ripple into mock…
satlead Mar 21, 2024
4747bde
fix: Emit events update
satlead Mar 21, 2024
9b4f88c
Merge branch 'main' of https://github.com/rdkcentral/Ripple into mock…
satlead Apr 2, 2024
0d17e21
fix: clippy errors
satlead Apr 2, 2024
d53382b
fix: unit tests
satlead Apr 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ members = [
"core/main",
"core/launcher",
"device/thunder",
"device/mock_device",
"distributor/general",
"examples/rpc_extn",
"examples/tm_extn",
Expand Down
1 change: 0 additions & 1 deletion core/main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ sd-notify = { version = "0.4.1", optional = true }
exitcode = "1.1.2"
rand = "0.8"


[build-dependencies]
vergen = "1"

Expand Down
2 changes: 1 addition & 1 deletion core/main/src/bootstrap/extn/load_extn_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl Bootstep<BootstrapState> for LoadExtensionsStep {
return Err(RippleError::BootstrapError);
}
} else {
error!("invalid channel builder in {}", path);
error!("failed loading builder in {}", path);
return Err(RippleError::BootstrapError);
}
} else {
Expand Down
1 change: 1 addition & 0 deletions core/main/src/state/bootstrap_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ impl ChannelsState {
let (gateway_tx, gateway_tr) = mpsc::channel(32);
let (app_req_tx, app_req_tr) = mpsc::channel(32);
let (ctx, ctr) = unbounded();

ChannelsState {
gateway_channel: TransientChannel::new(gateway_tx, gateway_tr),
app_req_channel: TransientChannel::new(app_req_tx, app_req_tr),
Expand Down
1 change: 0 additions & 1 deletion core/main/src/state/platform_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ impl PlatformState {
version: Option<String>,
) -> PlatformState {
let exclusory = ExclusoryImpl::get(&manifest);

Self {
extn_manifest,
cap_state: CapState::new(manifest.clone()),
Expand Down
6 changes: 2 additions & 4 deletions core/main/src/utils/rpc_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ use crate::{
state::platform_state::PlatformState,
};

pub use ripple_sdk::utils::rpc_utils::rpc_err;

pub const FIRE_BOLT_DEEPLINK_ERROR_CODE: i32 = -40400;
pub const DOWNSTREAM_SERVICE_UNAVAILABLE_ERROR_CODE: i32 = -50200;
pub const SESSION_NO_INTENT_ERROR_CODE: i32 = -40000;

pub fn rpc_err(msg: impl Into<String>) -> Error {
Error::Custom(msg.into())
}

/// Awaits a oneshot to respond. If the oneshot fails to repond, creates a generic
/// RPC internal error
pub async fn rpc_await_oneshot<T>(rx: oneshot::Receiver<T>) -> RpcResult<T> {
Expand Down
18 changes: 17 additions & 1 deletion core/sdk/src/extn/client/extn_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,22 @@ impl ExtnClient {
{
self.context_update(request);
}
// if its a request coming as an extn provider the extension is calling on itself.
// for eg an extension has a RPC Method provider and also a channel to process the
// requests this below impl will take care of sending the data back to the Extension
else if let Some(extn_id) = target_contract.is_extn_provider() {
if let Some(s) = self.get_extn_sender_with_extn_id(&extn_id) {
let new_message = message.clone();
tokio::spawn(async move {
if let Err(e) = s.send(new_message.into()).await {
error!("Error forwarding request {:?}", e)
}
});
} else {
error!("couldn't find the extension id registered the extn channel {:?} is not available", extn_id);
self.handle_no_processor_error(message);
}
}
// Forward the message to an extn sender
else if let Some(sender) = self.get_extn_sender_with_contract(target_contract)
{
Expand Down Expand Up @@ -600,7 +616,7 @@ impl ExtnClient {

/// Request method which accepts a impl [ExtnPayloadProvider] and uses the capability provided by the trait to send the request.
/// As part of the send process it adds a callback to asynchronously respond back to the caller when the response does get
/// received. This method can be called synchrnously with a timeout
/// received. This method can be called synchronously with a timeout
///
/// # Arguments
/// `payload` - impl [ExtnPayloadProvider]
Expand Down
7 changes: 3 additions & 4 deletions core/sdk/src/extn/client/extn_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,9 @@ pub trait ExtnRequestProcessor: ExtnStreamProcessor + Send + Sync + 'static {
///
/// # Returns
///
/// `Option<bool>` -> Used by [ExtnClient] to handle post processing
/// None - means not processed
/// Some(true) - Successful processing with status success
/// Some(false) - Successful processing with status error
/// `bool` -> Used by [ExtnClient] to handle post processing
/// `true` - Successful processing with status success
/// `false` - Successful processing with status error
async fn process_request(
state: Self::STATE,
msg: ExtnMessage,
Expand Down
6 changes: 4 additions & 2 deletions core/sdk/src/extn/client/extn_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ impl ExtnSender {
}

pub fn check_contract_fulfillment(&self, contract: RippleContract) -> bool {
if self.id.is_main() {
if self.id.is_main() || self.fulfills.contains(&contract.as_clear_string()) {
true
} else if let Ok(extn_id) = ExtnId::try_from(contract.as_clear_string()) {
self.id.eq(&extn_id)
} else {
self.fulfills.contains(&contract.as_clear_string())
false
}
}

Expand Down
131 changes: 126 additions & 5 deletions core/sdk/src/extn/extn_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
// SPDX-License-Identifier: Apache-2.0
//

use crate::utils::error::RippleError;
use crate::{
framework::ripple_contract::{ContractAdjective, RippleContract},
utils::error::RippleError,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;

use super::extn_client_message::{ExtnPayload, ExtnPayloadProvider, ExtnRequest, ExtnResponse};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtnClassId {
Expand Down Expand Up @@ -133,13 +140,36 @@ impl ExtnClassType {
/// Below capability means the given plugin offers a JsonRpsee rpc extension for a service named bridge
///
/// `ripple:extn:jsonrpsee:bridge`
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct ExtnId {
pub _type: ExtnType,
pub class: ExtnClassId,
pub service: String,
}

impl Serialize for ExtnId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> Deserialize<'de> for ExtnId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if let Ok(str) = String::deserialize(deserializer) {
if let Ok(id) = ExtnId::try_from(str) {
return Ok(id);
}
}
Err(serde::de::Error::unknown_variant("unknown", &["unknown"]))
}
}

impl ToString for ExtnId {
fn to_string(&self) -> String {
let r = format!(
Expand Down Expand Up @@ -406,9 +436,100 @@ impl ExtnId {
}
}

impl PartialEq for ExtnId {
fn eq(&self, other: &ExtnId) -> bool {
self._type == other._type && self.class == other.class
#[derive(Debug, Clone, PartialEq)]
pub struct ExtnProviderAdjective {
pub id: ExtnId,
}

impl Serialize for ExtnProviderAdjective {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.id.to_string())
}
}

impl<'de> Deserialize<'de> for ExtnProviderAdjective {
fn deserialize<D>(deserializer: D) -> Result<ExtnProviderAdjective, D::Error>
where
D: Deserializer<'de>,
{
if let Ok(str) = String::deserialize(deserializer) {
if let Ok(id) = ExtnId::try_from(str) {
return Ok(ExtnProviderAdjective { id });
}
}
Err(serde::de::Error::unknown_variant("unknown", &["unknown"]))
}
}

impl ContractAdjective for ExtnProviderAdjective {
fn get_contract(&self) -> RippleContract {
RippleContract::ExtnProvider(self.clone())
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ExtnProviderRequest {
pub value: Value,
pub id: ExtnId,
}

impl ExtnPayloadProvider for ExtnProviderRequest {
fn get_from_payload(payload: ExtnPayload) -> Option<Self> {
if let ExtnPayload::Request(ExtnRequest::Extn(value)) = payload {
if let Ok(v) = serde_json::from_value::<ExtnProviderRequest>(value) {
return Some(v);
}
}

None
}

fn get_extn_payload(&self) -> ExtnPayload {
ExtnPayload::Request(ExtnRequest::Extn(
serde_json::to_value(self.clone()).unwrap(),
))
}

fn contract() -> RippleContract {
// Will be replaced by the IEC before CExtnMessage conversion
RippleContract::ExtnProvider(ExtnProviderAdjective {
id: ExtnId::get_main_target("default".into()),
})
}

fn get_contract(&self) -> RippleContract {
RippleContract::ExtnProvider(ExtnProviderAdjective {
id: self.id.clone(),
})
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ExtnProviderResponse {
pub value: Value,
}

impl ExtnPayloadProvider for ExtnProviderResponse {
fn get_from_payload(payload: ExtnPayload) -> Option<Self> {
if let ExtnPayload::Response(ExtnResponse::Value(value)) = payload {
return Some(ExtnProviderResponse { value });
}

None
}

fn get_extn_payload(&self) -> ExtnPayload {
ExtnPayload::Response(ExtnResponse::Value(self.value.clone()))
}

fn contract() -> RippleContract {
// Will be replaced by the IEC before CExtnMessage conversion
RippleContract::ExtnProvider(ExtnProviderAdjective {
id: ExtnId::get_main_target("default".into()),
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/sdk/src/extn/ffi/ffi_library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub struct ExtnSymbolMetadata {
}

#[repr(C)]
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct CExtnMetadata {
pub name: String,
pub metadata: String,
Expand Down
7 changes: 2 additions & 5 deletions core/sdk/src/extn/ffi/ffi_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ mod tests {
// Create a mock CExtnMessage
let c_extn_message = CExtnMessage {
id: "test_id".to_string(),
requestor,
requestor: requestor.clone(),
target,
target_id: "".to_string(),
payload,
Expand All @@ -201,10 +201,7 @@ mod tests {
assert!(extn_message.is_ok(), "Expected Ok, but got Err");
if let Ok(extn_message) = extn_message {
assert_eq!(extn_message.id, "test_id");
assert_eq!(
extn_message.requestor,
ExtnId::new_channel(ExtnClassId::Device, "info".to_string())
);
assert_eq!(extn_message.requestor.to_string(), requestor);
assert_eq!(extn_message.target, RippleContract::DeviceInfo);
assert_eq!(extn_message.target_id, None);
assert_eq!(
Expand Down
28 changes: 27 additions & 1 deletion core/sdk/src/framework/ripple_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ use crate::{
session::{EventAdjective, PubSubAdjective, SessionAdjective},
storage_property::StorageAdjective,
},
extn::extn_id::ExtnProviderAdjective,
utils::{error::RippleError, serde_utils::SerdeClearString},
};
use jsonrpsee_core::DeserializeOwned;
use log::error;
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand Down Expand Up @@ -122,13 +124,14 @@ pub enum RippleContract {
/// the Session information based on their policies. Used by [crate::api::session::AccountSession]
Session(SessionAdjective),
RippleContext,
ExtnProvider(ExtnProviderAdjective),
AppCatalog,
Apps,
// Runtime ability for a given distributor to turn off a certian feature
RemoteFeatureControl,
}

pub trait ContractAdjective: serde::ser::Serialize {
pub trait ContractAdjective: serde::ser::Serialize + DeserializeOwned {
fn as_string(&self) -> String {
let adjective = SerdeClearString::as_clear_string(self);
if let Some(contract) = self.get_contract().get_adjective_contract() {
Expand Down Expand Up @@ -185,13 +188,27 @@ impl RippleContract {
Self::Session(adj) => Some(adj.as_string()),
Self::PubSub(adj) => Some(adj.as_string()),
Self::DeviceEvents(adj) => Some(adj.as_string()),
Self::ExtnProvider(adj) => Some(adj.id.to_string()),
_ => None,
}
}

fn get_contract_from_adjective<T: ContractAdjective>(str: &str) -> Option<RippleContract> {
match serde_json::from_str::<T>(str) {
Ok(v) => Some(v.get_contract()),
Err(e) => {
error!("contract parser_error={:?}", e);
None
}
}
}

pub fn from_adjective_string(contract: &str, adjective: &str) -> Option<Self> {
let adjective = format!("\"{}\"", adjective);
match contract {
"extn_provider" => {
return Self::get_contract_from_adjective::<ExtnProviderAdjective>(&adjective)
}
"storage" => match serde_json::from_str::<StorageAdjective>(&adjective) {
Ok(v) => return Some(v.get_contract()),
Err(e) => error!("contract parser_error={:?}", e),
Expand All @@ -217,6 +234,7 @@ impl RippleContract {
match self {
Self::Storage(_) => Some("storage".to_owned()),
Self::Session(_) => Some("session".to_owned()),
Self::ExtnProvider(_) => Some("extn_provider".to_owned()),
Self::PubSub(_) => Some("pubsub".to_owned()),
Self::DeviceEvents(_) => Some("device_events".to_owned()),
_ => None,
Expand All @@ -238,6 +256,14 @@ impl RippleContract {
}
None
}

pub fn is_extn_provider(&self) -> Option<String> {
if let RippleContract::ExtnProvider(e) = self {
Some(e.id.to_string())
} else {
None
}
}
}

#[derive(Debug, Clone, PartialEq)]
Expand Down
4 changes: 4 additions & 0 deletions core/sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ pub extern crate serde_json;
pub extern crate serde_yaml;
pub extern crate tokio;
pub extern crate uuid;

pub trait Mockable {
fn mock() -> Self;
}
Loading
Loading