use super::{CountResult, MissingFilterError, QueryFailedError, Uint64, schema};
use crate::{
api::v1::{ChainAddress, ServiceSelector, ServiceTypeId},
errors::{BlokliClientError, ErrorKind},
};
pub(crate) fn service_type_to_filter(service_type: ServiceTypeId) -> String {
format!("0x{}", hex::encode(service_type))
}
#[cfg(any(test, feature = "testing"))]
pub(crate) fn service_type_name(service_type: &ServiceTypeId) -> Option<&str> {
let len = service_type.iter().rposition(|byte| *byte != 0)? + 1;
let name = std::str::from_utf8(&service_type[..len]).ok()?;
name.bytes().all(|byte| byte.is_ascii_graphic()).then_some(name)
}
fn node_to_filter(node: ChainAddress) -> String {
hex::encode(node)
}
#[derive(cynic::QueryVariables, Debug, Default)]
pub struct ServiceVariables {
pub service_type: Option<String>,
pub node: Option<String>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct ServicePageVariables {
pub service_type: Option<String>,
pub node: Option<String>,
pub first: i32,
pub after: Option<Uint64>,
pub watermark: Option<Uint64>,
pub live_only: bool,
}
impl ServicePageVariables {
pub fn new(selector: ServiceSelector, after: Option<Uint64>, watermark: Option<Uint64>, live_only: bool) -> Self {
let filters = ServiceVariables::from(selector);
Self {
service_type: filters.service_type,
node: filters.node,
first: 1000,
after,
watermark,
live_only,
}
}
}
impl From<ServiceSelector> for ServiceVariables {
fn from(value: ServiceSelector) -> Self {
match value {
ServiceSelector::ServiceType(service_type) => ServiceVariables {
service_type: Some(service_type_to_filter(service_type)),
node: None,
},
ServiceSelector::Node(node) => ServiceVariables {
service_type: None,
node: Some(node_to_filter(node)),
},
ServiceSelector::ServiceTypeAndNode { service_type, node } => ServiceVariables {
service_type: Some(service_type_to_filter(service_type)),
node: Some(node_to_filter(node)),
},
ServiceSelector::Any => ServiceVariables::default(),
}
}
}
#[derive(cynic::QueryVariables, Debug, Default)]
pub struct ServiceTypeVariables {
pub service_type: Option<String>,
}
impl From<Option<ServiceTypeId>> for ServiceTypeVariables {
fn from(value: Option<ServiceTypeId>) -> Self {
ServiceTypeVariables {
service_type: value.map(service_type_to_filter),
}
}
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "QueryRoot", variables = "ServicePageVariables")]
pub struct QueryServices {
#[arguments(serviceType: $service_type, node: $node, first: $first, after: $after, watermark: $watermark, liveOnly: $live_only)]
pub services: ServicesResult,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "QueryRoot", variables = "ServiceVariables")]
pub struct QueryServiceCount {
#[arguments(serviceType: $service_type, node: $node)]
pub service_count: CountResult,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "QueryRoot", variables = "ServiceTypeVariables")]
pub struct QueryServiceTypes {
#[arguments(serviceType: $service_type)]
pub service_types: ServiceTypesResult,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "QueryRoot")]
pub struct QueryServiceRegistryConfig {
pub service_registry_config: ServiceRegistryConfigResult,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "SubscriptionRoot", variables = "ServiceVariables")]
pub struct SubscribeServices {
#[arguments(serviceType: $service_type, node: $node)]
pub service_updated: ServiceUpdate,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "SubscriptionRoot", variables = "ServiceTypeVariables")]
pub struct SubscribeServiceTypes {
#[arguments(serviceType: $service_type)]
pub service_type_updated: ServiceTypeUpdate,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "SubscriptionRoot")]
pub struct SubscribeServiceRegistryConfig {
pub service_registry_config_updated: ServiceRegistryConfig,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceEntry {
pub service_type: String,
pub node: String,
pub safe: String,
pub metadata: String,
pub registered_at: Uint64,
pub updated_at: Uint64,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceTypeInfo {
pub service_type: String,
pub owner: Option<String>,
pub requirement: Option<String>,
pub registration_burn: String,
pub update_burn: String,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceRegistryConfig {
pub type_registration_fee: String,
pub node_safe_registry: String,
}
#[derive(cynic::Enum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum ServiceUpdateKind {
#[cynic(rename = "REGISTERED")]
Registered,
#[cynic(rename = "UPDATED")]
Updated,
#[cynic(rename = "DEREGISTERED")]
Deregistered,
}
#[derive(cynic::Enum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum ServiceTypeUpdateKind {
#[cynic(rename = "REGISTERED")]
Registered,
#[cynic(rename = "OWNER_CHANGED")]
OwnerChanged,
#[cynic(rename = "REQUIREMENT_CHANGED")]
RequirementChanged,
#[cynic(rename = "REGISTRATION_BURN_CHANGED")]
RegistrationBurnChanged,
#[cynic(rename = "UPDATE_BURN_CHANGED")]
UpdateBurnChanged,
#[cynic(rename = "REGISTRATION_FEE_CHANGED")]
RegistrationFeeChanged,
#[cynic(rename = "REGISTRY_POINTER_CHANGED")]
RegistryPointerChanged,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceUpdate {
pub kind: ServiceUpdateKind,
pub service_type: String,
pub node: String,
pub entry: Option<ServiceEntry>,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceTypeUpdate {
pub kind: ServiceTypeUpdateKind,
pub service_type: Option<String>,
pub config: Option<ServiceTypeInfo>,
pub registry_config: Option<ServiceRegistryConfig>,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServicesList {
pub services: Vec<ServiceEntry>,
pub watermark: Uint64,
pub next_cursor: Option<Uint64>,
}
#[derive(Debug)]
pub struct ServicePage {
pub services: Vec<ServiceEntry>,
pub watermark: Uint64,
pub next_cursor: Option<Uint64>,
}
#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceTypesList {
pub service_types: Vec<ServiceTypeInfo>,
}
#[derive(cynic::InlineFragments, Debug)]
pub enum ServicesResult {
ServicesList(ServicesList),
MissingFilterError(MissingFilterError),
QueryFailedError(QueryFailedError),
#[cynic(fallback)]
Unknown,
}
impl From<ServicesResult> for Result<ServicePage, BlokliClientError> {
fn from(value: ServicesResult) -> Self {
match value {
ServicesResult::ServicesList(list) => Ok(ServicePage {
services: list.services,
watermark: list.watermark,
next_cursor: list.next_cursor,
}),
ServicesResult::MissingFilterError(e) => Err(e.into()),
ServicesResult::QueryFailedError(e) => Err(e.into()),
ServicesResult::Unknown => Err(ErrorKind::NoData.into()),
}
}
}
#[derive(cynic::InlineFragments, Debug)]
pub enum ServiceTypesResult {
ServiceTypesList(ServiceTypesList),
QueryFailedError(QueryFailedError),
#[cynic(fallback)]
Unknown,
}
#[derive(cynic::InlineFragments, Debug)]
pub enum ServiceRegistryConfigResult {
ServiceRegistryConfig(ServiceRegistryConfig),
QueryFailedError(QueryFailedError),
#[cynic(fallback)]
Unknown,
}
impl From<ServiceRegistryConfigResult> for Result<ServiceRegistryConfig, BlokliClientError> {
fn from(value: ServiceRegistryConfigResult) -> Self {
match value {
ServiceRegistryConfigResult::ServiceRegistryConfig(config) => Ok(config),
ServiceRegistryConfigResult::QueryFailedError(e) => Err(e.into()),
ServiceRegistryConfigResult::Unknown => Err(ErrorKind::NoData.into()),
}
}
}
impl From<ServiceTypesResult> for Result<Vec<ServiceTypeInfo>, BlokliClientError> {
fn from(value: ServiceTypesResult) -> Self {
match value {
ServiceTypesResult::ServiceTypesList(list) => Ok(list.service_types),
ServiceTypesResult::QueryFailedError(e) => Err(e.into()),
ServiceTypesResult::Unknown => Err(ErrorKind::NoData.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::{ServiceVariables, service_type_name, service_type_to_filter};
use crate::api::v1::ServiceSelector;
const GVPN_EXIT: [u8; 32] = [
0x67, 0x76, 0x70, 0x6e, 0x3a, 0x65, 0x78, 0x69, 0x74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
];
#[test]
fn service_type_filter_is_zero_padded_prefixed_hex() {
assert_eq!(
service_type_to_filter(GVPN_EXIT),
"0x6776706e3a657869740000000000000000000000000000000000000000000000"
);
}
#[test]
fn ascii_name_is_decoded_from_a_right_padded_id() {
assert_eq!(service_type_name(&GVPN_EXIT), Some("gvpn:exit"));
}
#[test]
fn ascii_name_rejects_ids_outside_the_convention() {
let mut interior_nul = GVPN_EXIT;
interior_nul[31] = b'x';
assert_eq!(service_type_name(&interior_nul), None);
let mut with_space = [0u8; 32];
with_space[..3].copy_from_slice(b"a b");
assert_eq!(service_type_name(&with_space), None);
assert_eq!(service_type_name(&[0u8; 32]), None);
}
#[test]
fn any_selector_sends_no_filters() {
let variables = ServiceVariables::from(ServiceSelector::Any);
assert!(variables.service_type.is_none());
assert!(variables.node.is_none());
}
#[test]
fn combined_selector_sends_both_filters() {
let variables = ServiceVariables::from(ServiceSelector::ServiceTypeAndNode {
service_type: GVPN_EXIT,
node: [0x11; 20],
});
assert_eq!(
variables.node.as_deref(),
Some("1111111111111111111111111111111111111111")
);
assert_eq!(
variables.service_type.as_deref(),
Some("0x6776706e3a657869740000000000000000000000000000000000000000000000")
);
}
}