# bee-api + Feature name: `api` + Start date: 2020-07-14 + RFC PR: [iotaledger/bee-rfcs#0000](https://github.com/iotaledger/bee-rfcs/pull/0000) + Bee issue: [iotaledger/bee#0000](https://github.com/iotaledger/bee/issues/0000) # Summary This RFC proposes a API crate (`bee-api`) to be added to the **Bee** framework that provides interfaces and conversions to easily integrate different APIs. [...] # Motivation [...] # Detailed design The design follows an layered approach and separates the `Service` from the `API`. ## Service The Service trait defines all services that a Bee node exposes. It hides internal implementation and changes, furthermore governs external access to data and functions. It can support multiple presentation layers such as web, mobile, UI, etc. Since it hides the internal logic and governs access, it helps to maintain layers above more easily. The `Service` trait is defined as follows: ``` #[async_trait] pub trait Service { fn node_info() -> NodeInfoResponse; fn transactions_by_bundle(params: TransactionsByBundleParams) -> Result<TransactionsByBundleResponse, ServiceError>; fn transaction_by_hash(params: TransactionByHashParams) -> TransactionByHashResponse; ... } ``` With function parameters and function return values like: ``` pub struct NodeInfoResponse { pub is_synced: bool, pub last_milestone_index: MilestoneIndex, pub last_milestone_hash: Option<Hash>, ... } pub struct TransactionsByBundleParams { pub entry: Hash, pub bundle: Hash, } pub struct TransactionsByBundleResponse { pub tx_refs: HashMap<Hash, TransactionRef>, } pub struct TransactionByHashParams { pub hash: Hash, } pub struct TransactionByHashResponse { pub tx_ref: Option<TransactionRef>, } ... ``` Since some service functions might return an error, e.g. if illegal parameters are provided or if an underlying function call returns an error, certain functions might return `Result<Response, ServiceError>`. ## Format Service functions might be called in different contexts. For example, a REST API might want to call a specific service function and pass the JSON parameters. An XML based API on the other hand might want to pass XML encoded parameters. To be able to call the service functions (see `Service` trait), input parameters need to be converted. A conversion from `JSON` to `TransactionsByBundleParams` could be defined like this: ``` impl TryFrom<&JsonValue> for TransactionsByBundleParams { type Error = &'static str; fn try_from(value: &JsonValue) -> Result<Self, Self::Error> { let entry = match value["entry"].as_str() { Some(str) => HashItem::try_from(str)?.0, None => return Err("can not find entry hash"), }; let bundle = match value["bundle"].as_str() { Some(str) => HashItem::try_from(str)?.0, None => return Err("can not find bundle hash"), }; Ok(TransactionsByBundleParams { entry, bundle }) } } ``` the conversion back from `TransactionsByBundleResponse` to `JSON` could be defined like this: ``` impl From<TransactionsByBundleResponse> for JsonValue { fn from(res: TransactionsByBundleResponse) -> Self { let mut json_obj = Map::new(); for (hash, tx_ref) in res.tx_refs.iter() { json_obj.insert( String::from(&HashItem(hash.clone())), JsonValue::from(&TransactionRefItem(tx_ref.clone())), ); } JsonValue::Object(json_obj) } } ``` Notable is the use of `HashItem` or `TransactionRefItem` in the conversions. These wrapper types are used to avoid issues with Rust's orphan rules: ``` pub struct HashItem(pub Hash); impl TryFrom<&str> for HashItem { type Error = &'static str; fn try_from(value: &str) -> Result<Self, Self::Error> { ... } } impl From<&HashItem> for String { fn from(value: &HashItem) -> Self { ... } } impl From<&HashItem> for JsonValue { fn from(value: &HashItem) -> Self { JsonValue::String(String::from(value)) } } ``` Since for example multiple services use `Hash` in parameter/response, conversions can be reused. This is very important exactly when the same types are used in different services and require more conversion work. An example for this is a reusable `TransactionRefItem` conversion: ``` pub struct TransactionRefItem(pub TransactionRef); impl From<&TransactionRefItem> for JsonValue { fn from(tx_ref: &TransactionRefItem) -> Self { let mut json_obj = Map::new(); json_obj.insert( String::from("payload"), JsonValue::String( tx_ref .0 .payload() .to_inner() .iter_trytes() .map(|trit| char::from(trit)) .collect::<String>(), ), ); json_obj.insert( String::from("address"), JsonValue::String( tx_ref .0 .address() .to_inner() .iter_trytes() .map(|trit| char::from(trit)) .collect::<String>(), ), ); json_obj.insert( String::from("value"), JsonValue::from(tx_ref.0.value().to_inner().clone()), ); ... } } ``` ## API trait The `Api` trait specifies which endpoints an API should implement, and therefore is different from the `Service` trait. ``` #[async_trait] pub trait Api { type Input; type Output; async fn node_info() -> Self::Output; async fn transactions_by_bundle(input: Self::Input) -> Self::Output; async fn transaction_by_hash(input: Self::Input) -> Self::Output; async fn transactions_by_hashes(input: Self::Input) -> Self::Output; } ``` The associated types are remarkable. Since some APIs might require a certain function signature this flexibility is needed. In the case of `warp` the Api gets implemented as following: ``` #[async_trait] impl Api for RestApi { type Input = JsonValue; type Output = Result<warp::reply::Json, warp::Rejection>; async fn node_info() -> Self::Output { Ok(warp::reply::json(&JsonValue::from(ServiceImpl::node_info()))) } ... } ``` These `Api` functions then can be easily use for to the `warp` routes. # Drawbacks [...] # Rationale and alternatives [...] # Unresolved questions - Should function params be built with a `new` constructor instead? And eventually return an error already there when illegal parameters where passed?