//! License preset templates for per-item license text. //! //! Creators choose from common presets or write custom terms. Template text //! uses `{year}` and `{owner}` placeholders, substituted at render time. use std::fmt; use std::str::FromStr; /// Available license presets. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LicensePreset { PersonalUse, RoyaltyFree, Mit, Apache2, CcBy4, CcByNc4, Cc0, Custom, } impl fmt::Display for LicensePreset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } impl FromStr for LicensePreset { type Err = String; fn from_str(s: &str) -> Result { match s { "personal_use" => Ok(Self::PersonalUse), "royalty_free" => Ok(Self::RoyaltyFree), "mit" => Ok(Self::Mit), "apache2" => Ok(Self::Apache2), "cc_by_4" => Ok(Self::CcBy4), "cc_by_nc_4" => Ok(Self::CcByNc4), "cc0" => Ok(Self::Cc0), "custom" => Ok(Self::Custom), other => Err(format!("invalid LicensePreset: {other}")), } } } impl LicensePreset { /// Database/form key string. pub fn as_str(&self) -> &'static str { match self { Self::PersonalUse => "personal_use", Self::RoyaltyFree => "royalty_free", Self::Mit => "mit", Self::Apache2 => "apache2", Self::CcBy4 => "cc_by_4", Self::CcByNc4 => "cc_by_nc_4", Self::Cc0 => "cc0", Self::Custom => "custom", } } /// Human-readable label for dropdown display. pub fn label(&self) -> &'static str { match self { Self::PersonalUse => "Personal Use Only", Self::RoyaltyFree => "Royalty-Free Commercial", Self::Mit => "MIT License", Self::Apache2 => "Apache License 2.0", Self::CcBy4 => "CC BY 4.0", Self::CcByNc4 => "CC BY-NC 4.0", Self::Cc0 => "Public Domain (CC0)", Self::Custom => "Custom", } } } /// All presets in display order. pub const ALL_PRESETS: &[LicensePreset] = &[ LicensePreset::PersonalUse, LicensePreset::RoyaltyFree, LicensePreset::Mit, LicensePreset::Apache2, LicensePreset::CcBy4, LicensePreset::CcByNc4, LicensePreset::Cc0, LicensePreset::Custom, ]; /// (value, label) pairs for template rendering. pub fn preset_options() -> Vec<(&'static str, &'static str)> { ALL_PRESETS.iter().map(|p| (p.as_str(), p.label())).collect() } // ── Template text constants ── const PERSONAL_USE_TEXT: &str = "\ Personal Use License Copyright (c) {year} {owner}. All rights reserved. This product is licensed for personal, non-commercial use only. You may \ not redistribute, resell, or sublicense this product or any derivative \ works. Commercial use requires a separate license from the copyright \ holder."; const ROYALTY_FREE_TEXT: &str = "\ Royalty-Free Commercial License Copyright (c) {year} {owner}. All rights reserved. You are granted a perpetual, non-exclusive, worldwide license to use this \ product in personal and commercial projects. You may not redistribute, \ resell, or sublicense the product itself (in whole or in part) as a \ standalone product. Crediting the original author is appreciated but \ not required."; const MIT_TEXT: &str = "\ MIT License Copyright (c) {year} {owner} Permission is hereby granted, free of charge, to any person obtaining a copy \ of this software and associated documentation files (the \"Software\"), to deal \ in the Software without restriction, including without limitation the rights \ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ copies of the Software, and to permit persons to whom the Software is \ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all \ copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR \ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE \ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER \ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, \ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE \ SOFTWARE."; const APACHE2_TEXT: &str = "\ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ Copyright (c) {year} {owner} Licensed under the Apache License, Version 2.0 (the \"License\"); you may not \ use this file except in compliance with the License. You may obtain a copy of \ the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed \ under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR \ CONDITIONS OF ANY KIND, either express or implied. See the License for the \ specific language governing permissions and limitations under the License."; const CC_BY_4_TEXT: &str = "\ Creative Commons Attribution 4.0 International (CC BY 4.0) Copyright (c) {year} {owner} You are free to: - Share: copy and redistribute the material in any medium or format - Adapt: remix, transform, and build upon the material for any purpose, \ even commercially Under the following terms: - Attribution: You must give appropriate credit, provide a link to the \ license, and indicate if changes were made. Full license text: https://creativecommons.org/licenses/by/4.0/legalcode"; const CC_BY_NC_4_TEXT: &str = "\ Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) Copyright (c) {year} {owner} You are free to: - Share: copy and redistribute the material in any medium or format - Adapt: remix, transform, and build upon the material Under the following terms: - Attribution: You must give appropriate credit, provide a link to the \ license, and indicate if changes were made. - NonCommercial: You may not use the material for commercial purposes. Full license text: https://creativecommons.org/licenses/by-nc/4.0/legalcode"; const CC0_TEXT: &str = "\ CC0 1.0 Universal (Public Domain Dedication) {owner} has dedicated this work to the public domain by waiving all rights \ under copyright law, including all related and neighboring rights, to the \ extent allowed by law. You can copy, modify, distribute, and perform the work, even for commercial \ purposes, all without asking permission. Full legal text: https://creativecommons.org/publicdomain/zero/1.0/legalcode"; /// Render the full license text for a preset, substituting `{year}` and `{owner}`. /// /// For `Custom`, the caller must supply the text via `custom_text`. If /// `custom_text` is `None` for a Custom preset, returns an empty string. pub fn render_license_text( preset: LicensePreset, owner: &str, year: i32, custom_text: Option<&str>, ) -> String { if preset == LicensePreset::Custom { return custom_text.unwrap_or("").to_string(); } let template = match preset { LicensePreset::PersonalUse => PERSONAL_USE_TEXT, LicensePreset::RoyaltyFree => ROYALTY_FREE_TEXT, LicensePreset::Mit => MIT_TEXT, LicensePreset::Apache2 => APACHE2_TEXT, LicensePreset::CcBy4 => CC_BY_4_TEXT, LicensePreset::CcByNc4 => CC_BY_NC_4_TEXT, LicensePreset::Cc0 => CC0_TEXT, LicensePreset::Custom => unreachable!(), }; template .replace("{year}", &year.to_string()) .replace("{owner}", owner) } #[cfg(test)] mod tests { use super::*; #[test] fn all_presets_round_trip() { for preset in ALL_PRESETS { let s = preset.as_str(); let parsed: LicensePreset = s.parse().unwrap(); assert_eq!(*preset, parsed); } } #[test] fn preset_labels_non_empty() { for preset in ALL_PRESETS { assert!(!preset.label().is_empty()); } } #[test] fn render_substitutes_placeholders() { let text = render_license_text(LicensePreset::Mit, "Alice", 2026, None); assert!(text.contains("2026")); assert!(text.contains("Alice")); assert!(!text.contains("{year}")); assert!(!text.contains("{owner}")); } #[test] fn render_all_presets_contain_owner() { for preset in ALL_PRESETS { if *preset == LicensePreset::Custom { continue; } let text = render_license_text(*preset, "TestOwner", 2026, None); assert!( text.contains("TestOwner"), "{:?} should contain owner name", preset ); } } #[test] fn render_custom_returns_custom_text() { let text = render_license_text( LicensePreset::Custom, "Owner", 2026, Some("My custom license terms."), ); assert_eq!(text, "My custom license terms."); } #[test] fn render_custom_without_text_returns_empty() { let text = render_license_text(LicensePreset::Custom, "Owner", 2026, None); assert_eq!(text, ""); } #[test] fn preset_options_has_all() { let opts = preset_options(); assert_eq!(opts.len(), ALL_PRESETS.len()); } #[test] fn invalid_preset_parse_fails() { assert!("nonexistent".parse::().is_err()); } // ── All template variants render without leftover placeholders ── #[test] fn render_all_presets_no_leftover_placeholders() { for preset in ALL_PRESETS { if *preset == LicensePreset::Custom { continue; } let text = render_license_text(*preset, "SomeOwner", 2025, None); assert!( !text.contains("{year}"), "{:?} still contains {{year}}", preset ); assert!( !text.contains("{owner}"), "{:?} still contains {{owner}}", preset ); } } #[test] fn render_all_presets_contain_year() { for preset in ALL_PRESETS { if *preset == LicensePreset::Custom || *preset == LicensePreset::Cc0 { continue; // CC0 template has no {year} placeholder } let text = render_license_text(*preset, "Owner", 2026, None); assert!( text.contains("2026"), "{:?} should contain the year", preset ); } } #[test] fn render_personal_use_contains_non_commercial() { let text = render_license_text(LicensePreset::PersonalUse, "Owner", 2026, None); assert!(text.contains("non-commercial")); } #[test] fn render_royalty_free_contains_perpetual() { let text = render_license_text(LicensePreset::RoyaltyFree, "Owner", 2026, None); assert!(text.contains("perpetual")); } #[test] fn render_mit_contains_permission_notice() { let text = render_license_text(LicensePreset::Mit, "Owner", 2026, None); assert!(text.contains("Permission is hereby granted")); assert!(text.contains("AS IS")); } #[test] fn render_apache2_contains_license_url() { let text = render_license_text(LicensePreset::Apache2, "Owner", 2026, None); assert!(text.contains("http://www.apache.org/licenses/LICENSE-2.0")); } #[test] fn render_cc_by_4_contains_attribution() { let text = render_license_text(LicensePreset::CcBy4, "Owner", 2026, None); assert!(text.contains("Attribution")); assert!(text.contains("creativecommons.org")); } #[test] fn render_cc_by_nc_4_contains_noncommercial() { let text = render_license_text(LicensePreset::CcByNc4, "Owner", 2026, None); assert!(text.contains("NonCommercial")); assert!(text.contains("creativecommons.org")); } #[test] fn render_cc0_contains_public_domain() { let text = render_license_text(LicensePreset::Cc0, "Owner", 2026, None); assert!(text.contains("public domain")); } // ── Special characters in variable values ── #[test] fn render_owner_with_special_characters() { let text = render_license_text(LicensePreset::Mit, "O'Brien & Co. ", 2026, None); assert!(text.contains("O'Brien & Co. ")); } #[test] fn render_owner_with_unicode() { let text = render_license_text(LicensePreset::Mit, "Müller GmbH", 2026, None); assert!(text.contains("Müller GmbH")); } #[test] fn render_owner_with_curly_braces() { // Ensure literal braces in owner name don't break substitution let text = render_license_text(LicensePreset::PersonalUse, "{braces}", 2026, None); assert!(text.contains("{braces}")); assert!(!text.contains("{year}")); } #[test] fn render_empty_owner() { let text = render_license_text(LicensePreset::Mit, "", 2026, None); assert!(text.contains("Copyright (c) 2026 ")); assert!(!text.contains("{owner}")); } // ── Custom template edge cases ── #[test] fn render_custom_ignores_owner_and_year() { let text = render_license_text( LicensePreset::Custom, "Ignored Owner", 9999, Some("No substitution happens for {year} or {owner}."), ); // Custom text is returned as-is, no substitution assert!(text.contains("{year}")); assert!(text.contains("{owner}")); } #[test] fn render_custom_with_empty_string() { let text = render_license_text(LicensePreset::Custom, "Owner", 2026, Some("")); assert_eq!(text, ""); } #[test] fn render_custom_with_multiline_text() { let custom = "Line 1\nLine 2\n\nLine 4"; let text = render_license_text(LicensePreset::Custom, "Owner", 2026, Some(custom)); assert_eq!(text, custom); } // ── Display / FromStr edge cases ── #[test] fn display_matches_as_str() { for preset in ALL_PRESETS { assert_eq!(format!("{preset}"), preset.as_str()); } } #[test] fn from_str_case_sensitive() { // Uppercase should fail assert!("MIT".parse::().is_err()); assert!("Personal_Use".parse::().is_err()); } #[test] fn from_str_empty_string_fails() { assert!("".parse::().is_err()); } #[test] fn preset_options_values_match_as_str() { let opts = preset_options(); for (i, preset) in ALL_PRESETS.iter().enumerate() { assert_eq!(opts[i].0, preset.as_str()); assert_eq!(opts[i].1, preset.label()); } } #[test] fn all_presets_have_unique_keys() { let keys: Vec<&str> = ALL_PRESETS.iter().map(|p| p.as_str()).collect(); let mut deduped = keys.clone(); deduped.sort(); deduped.dedup(); assert_eq!(keys.len(), deduped.len()); } #[test] fn all_presets_have_unique_labels() { let labels: Vec<&str> = ALL_PRESETS.iter().map(|p| p.label()).collect(); let mut deduped = labels.clone(); deduped.sort(); deduped.dedup(); assert_eq!(labels.len(), deduped.len()); } }