diff --git a/.agent/workflows/tdd.md b/.agent/workflows/tdd.md index 226032b..aac0c74 100644 --- a/.agent/workflows/tdd.md +++ b/.agent/workflows/tdd.md @@ -20,6 +20,7 @@ description: Test-Driven Development workflow with high safety and auditable res ## 4. Verification & Coverage - Ensure code coverage is >80% for new logic. - Run linters: `cargo clippy`. +- Run formatters: `cargo fmt`. ## 5. Commit & Audit - **Commit Message**: Use conventional commits (e.g., `feat:`, `fix:`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b7d1f4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing + +I would _love_ to have help on this, both because I'm an amateur programmer and a Rust newbie, and because the vision I am sketching out here will take a lot of work to realize. + +## Getting Started + +To build and test the project: + +```bash +# Clone the repository +git clone https://github.com/bdarcus/csln.git +cd csln + +# Build the project +cargo build + +# Run tests +cargo test + +# Run clippy for code quality checks +cargo clippy --all-targets --all-features + +# Format code +cargo fmt + +# Generate JSON schemas +cargo run --bin csln-schemas +``` + +## Development Standards + +To maintain code quality and a clean history, please follow these guidelines: + +1. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/). + - Subject: Max 50 characters. + - Body: Wrapped at 72 characters. + - DCO: All commits must be signed-off (`git commit -s`). + - Format: Use plain text only (no backsticks or Markdown) in commit bodies. +2. **Code Quality**: Ensure `cargo fmt` and `cargo clippy --workspace` pass before submitting. +3. **Performance**: For performance-related changes, provide before/after benchmark results from `cargo bench -p csln-processor`. +4. **Licensing**: New files must include the SPDX license header (MPL-2.0). + +## Project Structure + +- `csln/` - Core library with data models for styles, bibliography, and citations +- `cli/` - Command-line interface for processing citations +- `processor/` - Citation and bibliography processing engine + - `src/lib.rs` - Library entry point + - `src/processor.rs` - Main processor logic + - `src/types.rs` - Core data types + - `src/values.rs` - Value extraction logic + - `src/render.rs` - Rendering logic + +## How to Help + +Please contact me via discussions or the issue tracker, or by email, if you'd like to contribute. + +I licensed the code here under the same terms as [citeproc-rs][CSLRS], in case code might be shared between them. +I also understand the Mozilla 2.0 license is compatible with Apache. + +A note on citeproc-rs: + +In reviewing the code, it strikes me pieces of it obviously complement this code base. +In particular, it has been optimized for the Zotero use-case, where it provides real-time formatting, while I have focused of the batch-processing case. + +[CSLRS]: https://github.com/zotero/citeproc-rs diff --git a/Cargo.toml b/Cargo.toml index bd1c3eb..31ad232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,13 +11,13 @@ unsafe_code = "forbid" [workspace.lints.clippy] # not sure on what to turn on and off -complexity = { level = "allow", priority = -1 } +complexity = { level = "warn", priority = -1 } expect_used = "warn" large_enum_variant = "allow" needless_borrow = "warn" needless_question_mark = "warn" needless_return = "warn" -style = { level = "allow", priority = -1 } +style = { level = "warn", priority = -1 } unwrap_used = "warn" [profile.release] diff --git a/README.md b/README.md index 046843f..1ca8960 100644 --- a/README.md +++ b/README.md @@ -75,56 +75,8 @@ Doing so, however, will require sorting out details of how that process is manag ## Contributing -I would _love_ to have help on this, both because I'm an amateur programmer and a Rust newbie, and because the vision I am sketching out here will take a lot of work to realize. +Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to build, test, and help with the project. -### Getting Started - -To build and test the project: - -```bash -# Clone the repository -git clone https://github.com/bdarcus/csln.git -cd csln - -# Build the project -cargo build - -# Run tests -cargo test - -# Run clippy for code quality checks -cargo clippy --all-targets --all-features - -# Format code -cargo fmt - -# Generate JSON schemas -cargo run --bin csln-schemas -``` - -### Development Standards - -To maintain code quality and a clean history, please follow these guidelines: - -1. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/). - - Subject: Max 50 characters. - - Body: Wrapped at 72 characters. - - DCO: All commits must be signed-off (`git commit -s`). - - Format: Use plain text only (no backsticks or Markdown) in commit bodies. -2. **Code Quality**: Ensure `cargo fmt` and `cargo clippy --workspace` pass before submitting. -3. **Performance**: For performance-related changes, provide before/after benchmark results from `cargo bench -p csln-processor`. -4. **Licensing**: New files must include the SPDX license header (MPL-2.0). - -### Project Structure - -- `csln/` - Core library with data models for styles, bibliography, and citations -- `cli/` - Command-line interface for processing citations -- `processor/` - Citation and bibliography processing engine - - `src/lib.rs` - Library entry point - - `src/processor.rs` - Main processor logic - - `src/types.rs` - Core data types - - `src/values.rs` - Value extraction logic - - `src/render.rs` - Rendering logic ### How to Help diff --git a/cli/src/makeschemas.rs b/cli/src/makeschemas.rs index cce63c6..9e9bb3e 100644 --- a/cli/src/makeschemas.rs +++ b/cli/src/makeschemas.rs @@ -13,29 +13,51 @@ use csln::citation::CitationList; use csln::style::locale::Locale; use csln::style::Style; -fn main() { - fs::create_dir_all("schemas").expect("Failed to create directory 'schemas'"); +use anyhow::Context; + +fn main() -> anyhow::Result<()> { + fs::create_dir_all("schemas").context("Failed to create directory 'schemas'")?; let style_schema = schema_for!(Style); let citation_schema = schema_for!(CitationList); let bib_schema = schema_for!(InputBibliography); let locale_schema = schema_for!(Locale); - let style_json_output = serde_json::to_string_pretty(&style_schema).unwrap(); - let citation_json_output = serde_json::to_string_pretty(&citation_schema).unwrap(); - let bib_json_output = serde_json::to_string_pretty(&bib_schema).unwrap(); - let locale_json_output = serde_json::to_string_pretty(&locale_schema).unwrap(); - - let mut citation_file = File::create("schemas/citation.json").unwrap(); - let mut style_file = File::create("schemas/style.json").unwrap(); - let mut bib_file = File::create("schemas/bibliography.json").unwrap(); - let mut locale_file = File::create("schemas/locale.json").unwrap(); - style_file.write_all(style_json_output.as_bytes()).unwrap(); - citation_file.write_all(citation_json_output.as_bytes()).unwrap(); - bib_file.write_all(bib_json_output.as_bytes()).unwrap(); - locale_file.write_all(locale_json_output.as_bytes()).unwrap(); + let style_json_output = serde_json::to_string_pretty(&style_schema) + .context("Failed to serialize style schema")?; + let citation_json_output = serde_json::to_string_pretty(&citation_schema) + .context("Failed to serialize citation schema")?; + let bib_json_output = serde_json::to_string_pretty(&bib_schema) + .context("Failed to serialize bibliography schema")?; + let locale_json_output = serde_json::to_string_pretty(&locale_schema) + .context("Failed to serialize locale schema")?; + + let mut citation_file = File::create("schemas/citation.json") + .context("Failed to create schemas/citation.json")?; + let mut style_file = File::create("schemas/style.json") + .context("Failed to create schemas/style.json")?; + let mut bib_file = File::create("schemas/bibliography.json") + .context("Failed to create schemas/bibliography.json")?; + let mut locale_file = File::create("schemas/locale.json") + .context("Failed to create schemas/locale.json")?; + + style_file + .write_all(style_json_output.as_bytes()) + .context("Failed to write style schema")?; + citation_file + .write_all(citation_json_output.as_bytes()) + .context("Failed to write citation schema")?; + bib_file + .write_all(bib_json_output.as_bytes()) + .context("Failed to write bibliography schema")?; + locale_file + .write_all(locale_json_output.as_bytes()) + .context("Failed to write locale schema")?; + println!("Wrote bibliography schema to schemas/bibliography.json"); println!("Wrote citation schema to schemas/citation.json"); println!("Wrote style schema to schemas/style.json"); println!("Wrote locale schema to schemas/locale.json"); + + Ok(()) } diff --git a/csln/src/bibliography/mod.rs b/csln/src/bibliography/mod.rs index 86f1ec2..5a60dbf 100644 --- a/csln/src/bibliography/mod.rs +++ b/csln/src/bibliography/mod.rs @@ -10,3 +10,30 @@ pub use reference::InputReference; /// A bibliography is a collection of references. pub type InputBibliography = HashMap; + +#[cfg(test)] +mod tests { + use super::reference::Title; + use super::*; + + #[test] + fn test_input_bibliography_deserialization() { + let json = r#" + { + "ITEM-1": { + "id": "ITEM-1", + "type": "book", + "title": "Book Title", + "issued": "2020" + } + } + "#; + let bib: InputBibliography = serde_json::from_str(json).unwrap(); + assert_eq!(bib.len(), 1); + // Correct comparison with Title enum + assert_eq!( + bib.get("ITEM-1").unwrap().title().as_ref().unwrap(), + &Title::Single("Book Title".to_string()) + ); + } +} diff --git a/csln/src/citation/mod.rs b/csln/src/citation/mod.rs index 50ff2fd..1d9f870 100644 --- a/csln/src/citation/mod.rs +++ b/csln/src/citation/mod.rs @@ -97,3 +97,30 @@ pub enum LocatorTerm { Verse, Volume, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_citation_deserialization() { + let json = r#" + { + "citation_items": [ + { + "refId": "ITEM-1" + } + ], + "mode": "integral" + } + "#; + let citation: Citation = serde_json::from_str(json).unwrap(); + assert_eq!(citation.citation_items.len(), 1); + assert_eq!(citation.citation_items[0].ref_id, "ITEM-1"); + // Check enum matches integral + match citation.mode { + CitationModeType::Integral => (), + _ => panic!("Expected Integral mode"), + } + } +} diff --git a/csln/src/style/mod.rs b/csln/src/style/mod.rs index 200024f..f0823d1 100644 --- a/csln/src/style/mod.rs +++ b/csln/src/style/mod.rs @@ -70,3 +70,24 @@ pub enum Category { #[serde(rename = "social science")] SocialScience, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_style_deserialization_minimal() { + let json = r#" + { + "info": { + "title": "Minimal Style", + "id": "http://example.com/styles/minimal" + } + } + "#; + let style: Style = serde_json::from_str(json).unwrap(); + assert_eq!(style.info.title.as_ref().unwrap(), "Minimal Style"); + assert!(style.bibliography.is_none()); + assert!(style.citation.is_none()); + } +} diff --git a/processor/src/processor.rs b/processor/src/processor.rs index 26b9204..fa7c7d0 100644 --- a/processor/src/processor.rs +++ b/processor/src/processor.rs @@ -76,7 +76,7 @@ impl Processor { let sorted_references = self.sort_references(self.get_references()); let bibliography: ProcBibliography = sorted_references .par_iter() - .map(|reference| self.process_reference(*reference)) + .map(|reference| self.process_reference(reference)) .collect(); let citations = if self.citations.is_empty() { None