dewey_decimal/
lib.rs

1#![warn(missing_docs)]
2
3//! Simple wrapper around the Dewey Decimal Classification system
4//!
5//! Provides functionality for fetching information about Dewey Decimal classes, along with methods for traversing the class hierarchy.
6//!
7//! Classes are automatically generated from [OpenLibrary](https://raw.githubusercontent.com/internetarchive/openlibrary/refs/heads/master/openlibrary/components/LibraryExplorer/ddc.json), or generated from an included JSON file if unable.
8//! 
9//! ## Usage
10//! 
11//! ```rust
12//! use dewey_decimal::{Dewey, Class};
13//! 
14//! fn main() {
15//!     // Get the class representing "Computer science, knowledge & systems"
16//!     let comp_sci = Class::get("00").unwrap();
17//! 
18//!     // Gets all children in this class
19//!     let cs_classes = comp_sci.all_children()
20//! }
21//! ```
22//! 
23//! ## Features
24//!
25//! `dewey-decimal` supports several serialization utilities, which can be activated with feature flags
26//!
27//! | Feature           | Description                                                                       |
28//! |-------------------|-----------------------------------------------------------------------------------|
29//! | `serde`           | Supports `serde` serialization & deserialization on [Class] (enabled by default)  |
30//! | `specta`          | Supports `specta::Type` on [Class]                                                |
31//! | `schemars`        | Supports `schemars::JsonSchema` on [Class]                                        |
32//! | `bevy_reflect`    | Supports `bevy_reflect::Reflect` on [Class]                                       |
33
34use trie_rs::map::Trie;
35pub use trie_rs;
36
37include!(concat!(env!("OUT_DIR"), "/classes.rs"));
38
39static CLASSES: std::sync::LazyLock<Trie<u8, Class>> = std::sync::LazyLock::new(||
40    make_class_static()
41);
42
43/// Stateless struct for getting [Class] instances
44pub struct Dewey;
45
46impl Dewey {
47    /// Gets the underlying prefix trie ([crate::trie_rs::map::Trie])
48    ///
49    /// # Returns
50    ///
51    /// - `Trie<u8, Class>` - The underlying prefix trie
52    pub fn map(&self) -> Trie<u8, Class> {
53        CLASSES.to_owned()
54    }
55
56    /// Gets a [Vec] of all classes
57    /// 
58    /// # Returns
59    /// 
60    /// - `Vec<Class>` - Gigantic [Vec] of [Class] instances
61    pub fn all(&self) -> Vec<Class> {
62        self.map().iter().map(|item: (Vec<u8>, &Class)| item.1.clone())
63            .collect()
64    }
65
66    fn as_label(&self, code: impl AsRef<str>) -> Vec<u8> {
67        code.as_ref()
68            .to_string()
69            .trim_matches('X')
70            .chars()
71            .map(|c| c.to_string().parse::<u8>().unwrap())
72            .collect()
73    }
74
75    /// Gets a class by exact code match
76    ///
77    /// # Arguments
78    ///
79    /// - `code` (`impl AsRef<str>`) - Code to search for
80    ///
81    /// # Returns
82    ///
83    /// - `Option<Class>` - The [Class] that matches the provided code, or [None] if not found.
84    pub fn get_class(&self, code: impl AsRef<str>) -> Option<Class> {
85        self.map().exact_match(self.as_label(code)).cloned()
86    }
87
88    /// Returns all classes matching the provided prefix
89    ///
90    /// # Arguments
91    ///
92    /// - `code` (`impl AsRef<str>`) - Code to search for
93    ///
94    /// # Returns
95    ///
96    /// - `Vec<Class>` - [Vec] of [Class] instances matching the prefix
97    pub fn get_matches(&self, code: impl AsRef<str>) -> Vec<Class> {
98        self.map()
99            .predictive_search(self.as_label(code))
100            .map(|item: (Vec<u8>, &Class)| item.1.clone())
101            .collect()
102    }
103
104    /// Gets all the direct children of the class with the provided code
105    ///
106    /// # Arguments
107    ///
108    /// - `code` (`impl AsRef<str>`) - Code to search for
109    ///
110    /// # Returns
111    ///
112    /// - `Vec<Class>` - [Vec] of [Class] instances that are direct children of the specified prefix
113    pub fn get_direct_children(&self, code: impl AsRef<str>) -> Vec<Class> {
114        let code = code.as_ref().to_string();
115        self.get_matches(code.clone())
116            .into_iter()
117            .filter_map(|c| {
118                if c.code.len() == code.len() + 1 { Some(c) } else { None }
119            })
120            .collect()
121    }
122
123    /// Gets all children (not including the exact match itself)
124    ///
125    /// # Arguments
126    ///
127    /// - `code` (`impl AsRef<str>`) - Code to search for
128    ///
129    /// # Returns
130    ///
131    /// - `Vec<Class>` - [Vec] of all children of this prefix
132    pub fn get_all_children(&self, code: impl AsRef<str>) -> Vec<Class> {
133        let code = code.as_ref().to_string();
134        self.get_matches(code.clone())
135            .into_iter()
136            .filter_map(|c| {
137                if c.code == code { None } else { Some(c) }
138            })
139            .collect()
140    }
141
142    /// Gets the parent of the selected prefix, if any
143    ///
144    /// # Arguments
145    ///
146    /// - `code` (`impl AsRef<str>`) - Code to search for
147    ///
148    /// # Returns
149    ///
150    /// - `Option<Class>` - Parent of the selected [Class], if any
151    pub fn get_parent(&self, code: impl AsRef<str>) -> Option<Class> {
152        let mut code = code.as_ref().to_string();
153        if code.len() > 1 {
154            let _ = code.pop();
155            self.get_class(code)
156        } else {
157            None
158        }
159    }
160
161    /// Gets the top-level categories (codes `0` through `9`)
162    ///
163    /// # Returns
164    ///
165    /// - `Vec<Class>` - [Vec] of top-level classes
166    pub fn categories(&self) -> Vec<Class> {
167        "0123456789"
168            .chars()
169            .map(|c| self.get_class(c.to_string()).unwrap())
170            .collect()
171    }
172}
173
174impl Class {
175    /// Gets a class based on a provided code (exact match)
176    ///
177    /// # Arguments
178    ///
179    /// - `code` (`impl AsRef<str>`) - Code to search for
180    ///
181    /// # Returns
182    ///
183    /// - `Option<Self>` - A new [Class] if found, otherwise [None]
184    pub fn get(code: impl AsRef<str>) -> Option<Self> {
185        Dewey.get_class(code)
186    }
187
188    /// See [Dewey::get_matches]
189    pub fn matches(&self) -> Vec<Class> {
190        Dewey.get_matches(self.code.clone())
191    }
192
193    /// See [Dewey::get_all_children]
194    pub fn all_children(&self) -> Vec<Class> {
195        Dewey.get_all_children(self.code.clone())
196    }
197
198    /// See [Dewey::get_direct_children]
199    pub fn children(&self) -> Vec<Class> {
200        Dewey.get_direct_children(self.code.clone())
201    }
202
203    /// See [Dewey::get_parent]
204    pub fn parent(&self) -> Option<Class> {
205        Dewey.get_parent(self.code.clone())
206    }
207}
208
209#[cfg(test)]
210mod test {
211    use super::*;
212
213    #[test]
214    fn test_get() {
215        for (code, name) in vec![
216            ("247", "Church furnishings & related articles"),
217            ("19", "Modern Western philosophy (19th-century, 20th-century)"),
218            ("0", "Computer science, information & general works")
219        ] {
220            let result = Class::get(code);
221            assert!(result.is_some(), "Expected Some(...)!");
222            assert_eq!(result.unwrap().name, name.to_string(), "Names didn't match!");
223        }
224
225        assert!(Class::get("008").is_none(), "This code is unused!");
226    }
227
228    #[test]
229    fn test_matches() {
230        for (code, matches) in vec![("247", 1usize), ("09", 11usize), ("0", 98usize)] {
231            let result = Class::get(code);
232            assert!(result.is_some(), "Expected Some(...)!");
233            assert_eq!(result.unwrap().matches().len(), matches, "Unexpected number of matches");
234        }
235    }
236}