lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

capability.rs (16091B)


      1 //! Side-effect-free client capability reporting.
      2 
      3 use std::collections::{BTreeMap, BTreeSet};
      4 
      5 /// Stable runtime identity for an SDK capability.
      6 ///
      7 /// These values intentionally describe behavior rather than Cargo features.
      8 /// Construction is private so every reported ID comes from the governed
      9 /// catalog below.
     10 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     11 pub struct CapabilityId(&'static str);
     12 
     13 impl CapabilityId {
     14     /// Canonical storage operations.
     15     pub const CANONICAL_STORAGE: Self = Self("storage.canonical");
     16     /// Persistent storage operations.
     17     pub const PERSISTENT_STORAGE: Self = Self("storage.persistent");
     18     /// Storage backup and restore operations.
     19     pub const BACKUP_RESTORE: Self = Self("storage.backup-restore");
     20     /// Local signing.
     21     pub const LOCAL_SIGNING: Self = Self("signing.local");
     22     /// NIP-46 remote signing.
     23     pub const NIP46_SIGNING: Self = Self("signing.nip46");
     24     /// Nostr event fetch.
     25     pub const NOSTR_FETCH: Self = Self("transport.nostr.fetch");
     26     /// Nostr event delivery.
     27     pub const NOSTR_DELIVERY: Self = Self("transport.nostr.delivery");
     28     /// Reticulum event fetch preview.
     29     pub const RETICULUM_FETCH: Self = Self("transport.reticulum.fetch");
     30     /// Reticulum event delivery preview.
     31     pub const RETICULUM_DELIVERY: Self = Self("transport.reticulum.delivery");
     32     /// Mesh transport preview.
     33     pub const MESH_TRANSPORT: Self = Self("transport.mesh");
     34     /// SimpleX transport experiment.
     35     pub const SIMPLEX_TRANSPORT: Self = Self("transport.simplex");
     36     /// Daemon-mediated event delivery.
     37     pub const DAEMON_DELIVERY: Self = Self("transport.daemon.delivery");
     38     /// Inbound synchronization.
     39     pub const SYNC_PULL: Self = Self("sync.pull");
     40     /// Outbound synchronization.
     41     pub const SYNC_PUSH: Self = Self("sync.push");
     42     /// Farm event publication.
     43     pub const FARM_PUBLICATION: Self = Self("product.farm.publish");
     44     /// Listing event publication.
     45     pub const LISTING_PUBLICATION: Self = Self("product.listing.publish");
     46     /// Trade command execution.
     47     pub const TRADE_COMMANDS: Self = Self("product.trade.command");
     48     /// Trade queries.
     49     pub const TRADE_QUERIES: Self = Self("product.trade.query");
     50     /// Knowledge event support.
     51     pub const KNOWLEDGE_EVENTS: Self = Self("event.knowledge");
     52 
     53     /// Returns the stable presentation-independent identity.
     54     #[must_use]
     55     pub const fn as_str(self) -> &'static str {
     56         self.0
     57     }
     58 }
     59 
     60 impl std::fmt::Display for CapabilityId {
     61     fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     62         formatter.write_str(self.0)
     63     }
     64 }
     65 
     66 /// Product maturity independent of runtime availability.
     67 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     68 pub enum Maturity {
     69     /// Supported Release V1 behavior.
     70     Stable,
     71     /// Preserved pre-stable behavior with an explicit compatibility warning.
     72     Preview,
     73     /// Exploratory behavior without a compatibility commitment.
     74     Experimental,
     75 }
     76 
     77 /// Current runtime availability independent of compilation and configuration.
     78 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     79 pub enum Availability {
     80     /// The configured capability is ready.
     81     Available,
     82     /// The configured capability is usable with reduced functionality.
     83     Degraded,
     84     /// The capability is compiled but not currently usable.
     85     Unavailable,
     86     /// The capability is not supported by this build.
     87     Unsupported,
     88 }
     89 
     90 /// One immutable capability observation.
     91 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     92 pub struct CapabilityStatus {
     93     id: CapabilityId,
     94     compiled: bool,
     95     configured: bool,
     96     availability: Availability,
     97     maturity: Maturity,
     98 }
     99 
    100 impl CapabilityStatus {
    101     /// Returns the stable runtime identity.
    102     #[must_use]
    103     pub const fn id(self) -> CapabilityId {
    104         self.id
    105     }
    106 
    107     /// Returns whether support was compiled into this package.
    108     #[must_use]
    109     pub const fn is_compiled(self) -> bool {
    110         self.compiled
    111     }
    112 
    113     /// Returns whether the host configured the capability for this client.
    114     #[must_use]
    115     pub const fn is_configured(self) -> bool {
    116         self.configured
    117     }
    118 
    119     /// Returns the current side-effect-free availability observation.
    120     #[must_use]
    121     pub const fn availability(self) -> Availability {
    122         self.availability
    123     }
    124 
    125     /// Returns the independent product maturity classification.
    126     #[must_use]
    127     pub const fn maturity(self) -> Maturity {
    128         self.maturity
    129     }
    130 }
    131 
    132 /// Complete deterministic capability report for one client observation.
    133 #[derive(Clone, Debug, Eq, PartialEq)]
    134 pub struct CapabilityReport {
    135     statuses: Vec<CapabilityStatus>,
    136 }
    137 
    138 impl CapabilityReport {
    139     /// Returns all known capabilities in stable catalog order.
    140     pub fn iter(&self) -> impl ExactSizeIterator<Item = &CapabilityStatus> {
    141         self.statuses.iter()
    142     }
    143 
    144     /// Finds one capability by stable runtime identity.
    145     #[must_use]
    146     pub fn get(&self, id: CapabilityId) -> Option<CapabilityStatus> {
    147         self.statuses.iter().copied().find(|status| status.id == id)
    148     }
    149 }
    150 
    151 #[derive(Clone, Copy)]
    152 struct Definition {
    153     id: CapabilityId,
    154     maturity: Maturity,
    155     compiled: bool,
    156 }
    157 
    158 const CATALOG: &[Definition] = &[
    159     Definition::stable(CapabilityId::CANONICAL_STORAGE, true),
    160     Definition::stable(CapabilityId::PERSISTENT_STORAGE, cfg!(feature = "sqlite")),
    161     Definition::stable(CapabilityId::BACKUP_RESTORE, true),
    162     Definition::stable(CapabilityId::LOCAL_SIGNING, cfg!(feature = "local-signing")),
    163     Definition::stable(CapabilityId::NIP46_SIGNING, cfg!(feature = "nip46")),
    164     Definition::stable(CapabilityId::NOSTR_FETCH, cfg!(feature = "nostr")),
    165     Definition::stable(CapabilityId::NOSTR_DELIVERY, cfg!(feature = "nostr")),
    166     Definition::preview(CapabilityId::RETICULUM_FETCH),
    167     Definition::preview(CapabilityId::RETICULUM_DELIVERY),
    168     Definition::experimental(CapabilityId::MESH_TRANSPORT),
    169     Definition::experimental(CapabilityId::SIMPLEX_TRANSPORT),
    170     Definition::stable(CapabilityId::DAEMON_DELIVERY, cfg!(feature = "radrootsd")),
    171     Definition::stable(CapabilityId::SYNC_PULL, cfg!(feature = "sync")),
    172     Definition::stable(CapabilityId::SYNC_PUSH, cfg!(feature = "sync")),
    173     Definition::stable(CapabilityId::FARM_PUBLICATION, true),
    174     Definition::stable(CapabilityId::LISTING_PUBLICATION, true),
    175     Definition::stable(CapabilityId::TRADE_COMMANDS, true),
    176     Definition::stable(CapabilityId::TRADE_QUERIES, true),
    177     Definition::stable(CapabilityId::KNOWLEDGE_EVENTS, cfg!(feature = "knowledge")),
    178 ];
    179 
    180 impl Definition {
    181     const fn stable(id: CapabilityId, compiled: bool) -> Self {
    182         Self {
    183             id,
    184             maturity: Maturity::Stable,
    185             compiled,
    186         }
    187     }
    188 
    189     const fn preview(id: CapabilityId) -> Self {
    190         Self {
    191             id,
    192             maturity: Maturity::Preview,
    193             compiled: false,
    194         }
    195     }
    196 
    197     const fn experimental(id: CapabilityId) -> Self {
    198         Self {
    199             id,
    200             maturity: Maturity::Experimental,
    201             compiled: false,
    202         }
    203     }
    204 }
    205 
    206 pub(crate) struct Context<'a> {
    207     pub(crate) storage: bool,
    208     pub(crate) signer: bool,
    209     pub(crate) source: bool,
    210     pub(crate) sink: bool,
    211     pub(crate) sync: bool,
    212     pub(crate) source_availability: Availability,
    213     pub(crate) sink_availability: Availability,
    214     pub(crate) lifecycle_availability: Availability,
    215     pub(crate) explicitly_configured: &'a BTreeSet<CapabilityId>,
    216     pub(crate) overrides: &'a BTreeMap<CapabilityId, Availability>,
    217 }
    218 
    219 pub(crate) fn report(context: Context<'_>) -> CapabilityReport {
    220     let statuses = CATALOG
    221         .iter()
    222         .map(|definition| {
    223             let configured = configured(definition.id, &context);
    224             let availability = if !definition.compiled {
    225                 Availability::Unsupported
    226             } else if !configured {
    227                 Availability::Unavailable
    228             } else if context.lifecycle_availability != Availability::Available {
    229                 context.lifecycle_availability
    230             } else {
    231                 context
    232                     .overrides
    233                     .get(&definition.id)
    234                     .copied()
    235                     .unwrap_or_else(|| directional_availability(definition.id, &context))
    236             };
    237             CapabilityStatus {
    238                 id: definition.id,
    239                 compiled: definition.compiled,
    240                 configured,
    241                 availability,
    242                 maturity: definition.maturity,
    243             }
    244         })
    245         .collect();
    246     CapabilityReport { statuses }
    247 }
    248 
    249 fn directional_availability(id: CapabilityId, context: &Context<'_>) -> Availability {
    250     match id {
    251         CapabilityId::NOSTR_FETCH | CapabilityId::SYNC_PULL => context.source_availability,
    252         CapabilityId::NOSTR_DELIVERY
    253         | CapabilityId::SYNC_PUSH
    254         | CapabilityId::FARM_PUBLICATION
    255         | CapabilityId::LISTING_PUBLICATION
    256         | CapabilityId::TRADE_COMMANDS => context.sink_availability,
    257         _ => context.lifecycle_availability,
    258     }
    259 }
    260 
    261 fn configured(id: CapabilityId, context: &Context<'_>) -> bool {
    262     match id {
    263         CapabilityId::CANONICAL_STORAGE
    264         | CapabilityId::BACKUP_RESTORE
    265         | CapabilityId::TRADE_QUERIES => context.storage,
    266         CapabilityId::SYNC_PULL => (context.sync, context.source) == (true, true),
    267         CapabilityId::SYNC_PUSH => (context.sync, context.sink) == (true, true),
    268         CapabilityId::FARM_PUBLICATION
    269         | CapabilityId::LISTING_PUBLICATION
    270         | CapabilityId::TRADE_COMMANDS => (context.signer, context.sink) == (true, true),
    271         CapabilityId::KNOWLEDGE_EVENTS => cfg!(feature = "knowledge"),
    272         CapabilityId::RETICULUM_FETCH
    273         | CapabilityId::RETICULUM_DELIVERY
    274         | CapabilityId::MESH_TRANSPORT
    275         | CapabilityId::SIMPLEX_TRANSPORT => false,
    276         _ => context.explicitly_configured.contains(&id),
    277     }
    278 }
    279 
    280 #[cfg(test)]
    281 mod tests {
    282     use super::*;
    283 
    284     #[test]
    285     fn catalog_ids_are_unique_and_not_feature_names() {
    286         for (index, definition) in CATALOG.iter().enumerate() {
    287             assert!(
    288                 !CATALOG[..index]
    289                     .iter()
    290                     .any(|candidate| candidate.id == definition.id),
    291                 "duplicate capability {}",
    292                 definition.id
    293             );
    294             assert!(definition.id.as_str().contains('.'));
    295         }
    296     }
    297 
    298     #[test]
    299     fn maturity_and_unsupported_preview_states_are_independent() {
    300         let overrides = BTreeMap::new();
    301         let explicitly_configured = BTreeSet::new();
    302         let report = report(Context {
    303             storage: true,
    304             signer: false,
    305             source: false,
    306             sink: false,
    307             sync: false,
    308             source_availability: Availability::Unavailable,
    309             sink_availability: Availability::Unavailable,
    310             lifecycle_availability: Availability::Available,
    311             explicitly_configured: &explicitly_configured,
    312             overrides: &overrides,
    313         });
    314         let storage = report
    315             .get(CapabilityId::CANONICAL_STORAGE)
    316             .expect("storage");
    317         assert!(storage.is_compiled());
    318         assert!(storage.is_configured());
    319         assert_eq!(storage.availability(), Availability::Available);
    320         assert_eq!(storage.maturity(), Maturity::Stable);
    321 
    322         let knowledge = report
    323             .get(CapabilityId::KNOWLEDGE_EVENTS)
    324             .expect("knowledge");
    325         assert_eq!(knowledge.is_compiled(), cfg!(feature = "knowledge"));
    326         assert_eq!(knowledge.is_configured(), cfg!(feature = "knowledge"));
    327 
    328         let reticulum = report
    329             .get(CapabilityId::RETICULUM_FETCH)
    330             .expect("reticulum");
    331         assert!(!reticulum.is_compiled());
    332         assert!(!reticulum.is_configured());
    333         assert_eq!(reticulum.availability(), Availability::Unsupported);
    334         assert_eq!(reticulum.maturity(), Maturity::Preview);
    335 
    336         let mesh = report.get(CapabilityId::MESH_TRANSPORT).expect("mesh");
    337         assert_eq!(mesh.maturity(), Maturity::Experimental);
    338     }
    339 
    340     #[test]
    341     fn configuration_and_availability_matrix_covers_every_decision_path() {
    342         let explicitly_configured = BTreeSet::from([
    343             CapabilityId::PERSISTENT_STORAGE,
    344             CapabilityId::NIP46_SIGNING,
    345             CapabilityId::NOSTR_FETCH,
    346         ]);
    347         let overrides = BTreeMap::from([
    348             (CapabilityId::PERSISTENT_STORAGE, Availability::Degraded),
    349             (CapabilityId::NOSTR_FETCH, Availability::Unavailable),
    350         ]);
    351 
    352         let complete = report(Context {
    353             storage: false,
    354             signer: true,
    355             source: true,
    356             sink: true,
    357             sync: true,
    358             source_availability: Availability::Available,
    359             sink_availability: Availability::Degraded,
    360             lifecycle_availability: Availability::Available,
    361             explicitly_configured: &explicitly_configured,
    362             overrides: &overrides,
    363         });
    364         assert!(
    365             complete
    366                 .get(CapabilityId::SYNC_PULL)
    367                 .expect("pull")
    368                 .is_configured()
    369         );
    370         assert_eq!(
    371             complete
    372                 .get(CapabilityId::SYNC_PUSH)
    373                 .expect("push")
    374                 .availability(),
    375             if cfg!(feature = "sync") {
    376                 Availability::Degraded
    377             } else {
    378                 Availability::Unsupported
    379             }
    380         );
    381         assert_eq!(
    382             complete
    383                 .get(CapabilityId::SYNC_PULL)
    384                 .expect("pull")
    385                 .availability(),
    386             if cfg!(feature = "sync") {
    387                 Availability::Available
    388             } else {
    389                 Availability::Unsupported
    390             }
    391         );
    392         assert!(
    393             complete
    394                 .get(CapabilityId::SYNC_PUSH)
    395                 .expect("push")
    396                 .is_configured()
    397         );
    398         assert!(
    399             complete
    400                 .get(CapabilityId::FARM_PUBLICATION)
    401                 .expect("farm")
    402                 .is_configured()
    403         );
    404         assert_eq!(
    405             complete
    406                 .get(CapabilityId::NOSTR_FETCH)
    407                 .expect("fetch")
    408                 .availability(),
    409             if cfg!(feature = "nostr") {
    410                 Availability::Unavailable
    411             } else {
    412                 Availability::Unsupported
    413             }
    414         );
    415 
    416         let partial = report(Context {
    417             storage: false,
    418             signer: true,
    419             source: false,
    420             sink: false,
    421             sync: true,
    422             source_availability: Availability::Unavailable,
    423             sink_availability: Availability::Unavailable,
    424             lifecycle_availability: Availability::Degraded,
    425             explicitly_configured: &explicitly_configured,
    426             overrides: &overrides,
    427         });
    428         assert!(
    429             !partial
    430                 .get(CapabilityId::SYNC_PULL)
    431                 .expect("pull")
    432                 .is_configured()
    433         );
    434         assert!(
    435             !partial
    436                 .get(CapabilityId::SYNC_PUSH)
    437                 .expect("push")
    438                 .is_configured()
    439         );
    440         assert!(
    441             !partial
    442                 .get(CapabilityId::TRADE_COMMANDS)
    443                 .expect("trade")
    444                 .is_configured()
    445         );
    446         if partial
    447             .get(CapabilityId::NIP46_SIGNING)
    448             .expect("nip46")
    449             .is_compiled()
    450         {
    451             assert_eq!(
    452                 partial
    453                     .get(CapabilityId::NIP46_SIGNING)
    454                     .expect("nip46")
    455                     .availability(),
    456                 Availability::Degraded
    457             );
    458         }
    459         assert_eq!(complete.iter().len(), CATALOG.len());
    460         assert_eq!(
    461             CapabilityId::NOSTR_FETCH.to_string(),
    462             "transport.nostr.fetch"
    463         );
    464     }
    465 }