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}