├── .envrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── documented-macros ├── Cargo.toml └── src │ ├── attr_impl.rs │ ├── config.rs │ ├── config │ ├── attr.rs │ ├── customise_core.rs │ ├── derive.rs │ └── derive_fields.rs │ ├── derive_impl.rs │ ├── lib.rs │ └── util.rs ├── documented-test ├── Cargo.toml └── src │ ├── attr.rs │ ├── attr │ └── docs_const.rs │ ├── derive.rs │ ├── derive │ ├── documented.rs │ ├── documented_fields.rs │ ├── documented_fields_opt.rs │ ├── documented_opt.rs │ ├── documented_variants.rs │ └── documented_variants_opt.rs │ └── lib.rs ├── flake.lock ├── flake.nix ├── lib ├── Cargo.toml └── src │ └── lib.rs ├── rustfmt.toml └── shell.nix /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 30 8 | groups: 9 | dependencies: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: monthly 16 | open-pull-requests-limit: 30 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | jobs: 9 | get-msrv: 10 | name: Get declared MSRV from Cargo.toml 11 | runs-on: ubuntu-latest 12 | outputs: 13 | msrv: ${{ steps.get_msrv.outputs.msrv }} 14 | steps: 15 | - name: Install ripgrep 16 | run: sudo apt-get install -y ripgrep 17 | 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Get MSRV 22 | id: get_msrv 23 | run: rg '^\s*rust-version\s*=\s*"(\d+(\.\d+){0,2})"' --replace 'msrv=$1' Cargo.toml >> "$GITHUB_OUTPUT" 24 | 25 | check: 26 | name: Run checks 27 | runs-on: ubuntu-latest 28 | needs: get-msrv 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | rust: 33 | - ${{ needs.get-msrv.outputs.msrv }} 34 | - stable 35 | - nightly 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | - name: Install Rust 41 | uses: dtolnay/rust-toolchain@master 42 | with: 43 | toolchain: ${{ matrix.rust }} 44 | components: clippy, rustfmt 45 | 46 | - name: Check formatting 47 | run: cargo fmt --all -- --check 48 | 49 | - name: Run clippy 50 | run: | 51 | cargo clippy --workspace -- -D warnings 52 | cargo clippy --workspace --no-default-features -- -D warnings 53 | 54 | 55 | - name: Run tests 56 | run: | 57 | cargo test --workspace 58 | cargo test --workspace --no-default-features 59 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # This workflow is manually triggered. 2 | 3 | name: publish-crate 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | publish-to-crates-io: 9 | name: Publish to crates.io 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Install Rust 16 | uses: dtolnay/rust-toolchain@master 17 | with: 18 | toolchain: stable 19 | 20 | - name: Run cargo publish 21 | uses: katyo/publish-crates@v2 22 | with: 23 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | 4 | /.direnv 5 | /.vscode 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["lib", "documented-macros", "documented-test"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | authors = [ 7 | "cyqsimon <28627918+cyqsimon@users.noreply.github.com>", 8 | "Uriel ", 9 | "Sese Mueller ", 10 | "Lauréline ", 11 | ] 12 | categories = ["rust-patterns"] 13 | edition = "2021" 14 | keywords = ["documentation", "proc-macro", "reflection"] 15 | license = "MIT" 16 | readme = "README.md" 17 | repository = "https://github.com/cyqsimon/documented" 18 | rust-version = "1.70.0" 19 | version = "0.9.1" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 [cyqsimon <28627918+cyqsimon@users.noreply.github.com>] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # documented 2 | 3 | Derive and attribute macros for accessing your type's documentation at runtime 4 | 5 | - [crates.io](https://crates.io/crates/documented) 6 | - [docs.rs](https://docs.rs/documented/latest/documented/) 7 | 8 | ## Quick start 9 | 10 | ```rust 11 | use documented::{Documented, DocumentedFields, DocumentedVariants}; 12 | 13 | /// Trying is the first step to failure. 14 | #[derive(Documented, DocumentedFields, DocumentedVariants)] 15 | enum AlwaysPlay { 16 | /// And Kb8. 17 | #[allow(dead_code)] 18 | Kb1, 19 | /// But only if you are white. 20 | F6, 21 | } 22 | 23 | // Documented 24 | assert_eq!(AlwaysPlay::DOCS, "Trying is the first step to failure."); 25 | 26 | // DocumentedFields 27 | assert_eq!( 28 | AlwaysPlay::FIELD_DOCS, 29 | ["And Kb8.", "But only if you are white."] 30 | ); 31 | assert_eq!(AlwaysPlay::get_field_docs("Kb1"), Ok("And Kb8.")); 32 | 33 | // DocumentedVariants 34 | assert_eq!( 35 | AlwaysPlay::F6.get_variant_docs(), 36 | "But only if you are white." 37 | ); 38 | ``` 39 | -------------------------------------------------------------------------------- /documented-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Derive and attribute macros for `documented`" 3 | edition.workspace = true 4 | license.workspace = true 5 | name = "documented-macros" 6 | repository.workspace = true 7 | rust-version.workspace = true 8 | version.workspace = true 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | convert_case = "0.8.0" 15 | itertools = { version = "0.14.0", optional = true } 16 | optfield = { version = "0.4.0", optional = true } 17 | proc-macro2 = "1.0.93" 18 | quote = "1.0.38" 19 | strum = { version = "0.26.3", features = ["derive"], optional = true } 20 | syn = { version = "2.0.98", features = ["full", "extra-traits"] } 21 | 22 | [dev-dependencies] 23 | documented = { path = "../lib" } 24 | 25 | [features] 26 | customise = ["dep:itertools", "dep:optfield", "dep:strum"] 27 | -------------------------------------------------------------------------------- /documented-macros/src/attr_impl.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of the attribute macros. 2 | 3 | use convert_case::{Case, Casing}; 4 | use proc_macro2::{Span, TokenStream}; 5 | use quote::quote; 6 | use syn::{Error, Ident, Item}; 7 | 8 | #[cfg(feature = "customise")] 9 | use crate::config::attr::AttrCustomisations; 10 | use crate::{ 11 | config::attr::AttrConfig, 12 | util::{get_docs, get_vis_name_attrs}, 13 | }; 14 | 15 | pub fn docs_const_impl( 16 | item: Item, 17 | #[cfg(feature = "customise")] customisations: AttrCustomisations, 18 | ) -> syn::Result { 19 | #[cfg(not(feature = "customise"))] 20 | let config = AttrConfig::default(); 21 | #[cfg(feature = "customise")] 22 | let config = AttrConfig::default().with_customisations(customisations); 23 | 24 | let (item_vis, item_name, attrs) = get_vis_name_attrs(&item)?; 25 | 26 | let docs = match (get_docs(attrs, config.trim)?, config.default_value) { 27 | (Some(docs), _) => Ok(quote! { #docs }), 28 | (None, Some(default)) => Ok(quote! { #default }), 29 | (None, None) => Err(Error::new_spanned(&item, "Missing doc comments")), 30 | }?; 31 | 32 | let const_vis = config.custom_vis.unwrap_or(item_vis); 33 | let const_name = config 34 | .custom_name 35 | .unwrap_or_else(|| format!("{}_DOCS", item_name.to_case(Case::UpperSnake))); 36 | let const_ident = Ident::new(&const_name, Span::call_site()); 37 | 38 | Ok(quote! { 39 | #item 40 | #const_vis const #const_ident: &'static str = #docs; 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /documented-macros/src/config.rs: -------------------------------------------------------------------------------- 1 | pub mod attr; 2 | #[cfg(feature = "customise")] 3 | pub mod customise_core; 4 | pub mod derive; 5 | pub mod derive_fields; 6 | -------------------------------------------------------------------------------- /documented-macros/src/config/attr.rs: -------------------------------------------------------------------------------- 1 | use syn::{Expr, Visibility}; 2 | 3 | /// Configurable options for attribute macros via helper attributes. 4 | /// 5 | /// Initial values are set to default. 6 | #[cfg_attr(feature = "customise", optfield::optfield( 7 | pub AttrCustomisations, 8 | attrs = add(derive(Default)), 9 | merge_fn = pub apply_customisations, 10 | doc = "Parsed user-defined customisations of configurable options.\n\ 11 | \n\ 12 | Expected parse stream format: ` = , = , ...`" 13 | ))] 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | pub struct AttrConfig { 16 | // optfield does not rewrap `Option` by default, which is the desired behavior 17 | // see https://docs.rs/optfield/latest/optfield/#rewrapping-option-fields 18 | pub custom_vis: Option, 19 | pub custom_name: Option, 20 | pub default_value: Option, 21 | pub trim: bool, 22 | } 23 | impl Default for AttrConfig { 24 | fn default() -> Self { 25 | Self { 26 | custom_vis: None, 27 | custom_name: None, 28 | default_value: None, 29 | trim: true, 30 | } 31 | } 32 | } 33 | 34 | #[cfg(feature = "customise")] 35 | mod customise { 36 | use syn::{ 37 | parse::{Parse, ParseStream}, 38 | punctuated::Punctuated, 39 | Token, 40 | }; 41 | 42 | use crate::config::{ 43 | attr::{AttrConfig, AttrCustomisations}, 44 | customise_core::{ensure_unique_options, ConfigOption, ConfigOptionData}, 45 | }; 46 | 47 | impl AttrConfig { 48 | /// Return a new instance of this config with customisations applied. 49 | pub fn with_customisations(mut self, customisations: AttrCustomisations) -> Self { 50 | self.apply_customisations(customisations); 51 | self 52 | } 53 | } 54 | 55 | impl Parse for AttrCustomisations { 56 | fn parse(input: ParseStream) -> syn::Result { 57 | use ConfigOptionData as Data; 58 | 59 | let opts = Punctuated::::parse_terminated(input)? 60 | .into_iter() 61 | .collect::>(); 62 | 63 | ensure_unique_options(&opts)?; 64 | 65 | let mut config = Self::default(); 66 | for opt in opts { 67 | // I'd love to macro this if declarative macros can expand to a full match arm, 68 | // but no: https://github.com/rust-lang/rfcs/issues/2654 69 | match opt.data { 70 | Data::RenameAll(..) => Err(syn::Error::new( 71 | opt.span, 72 | "This config option is not applicable here", 73 | ))?, 74 | Data::Vis(vis) => { 75 | config.custom_vis.replace(vis); 76 | } 77 | Data::Rename(name) => { 78 | config.custom_name.replace(name.value()); 79 | } 80 | Data::Default(expr) => { 81 | config.default_value.replace(expr); 82 | } 83 | Data::Trim(trim) => { 84 | config.trim.replace(trim.value()); 85 | } 86 | } 87 | } 88 | Ok(config) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /documented-macros/src/config/customise_core.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use proc_macro2::Span; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | punctuated::Punctuated, 6 | spanned::Spanned, 7 | Attribute, Error, Expr, LitBool, LitStr, Meta, Token, Visibility, 8 | }; 9 | 10 | mod kw { 11 | use syn::custom_keyword; 12 | 13 | custom_keyword!(vis); 14 | custom_keyword!(rename_all); 15 | custom_keyword!(rename); 16 | custom_keyword!(default); 17 | custom_keyword!(trim); 18 | 19 | // recognised old keywords 20 | // error when used 21 | custom_keyword!(name); 22 | } 23 | 24 | /// A configuration option that includes the span info. Each kind of 25 | /// customisation struct may choose to accept or reject any of them. 26 | /// 27 | /// Expected parse stream format: ` = `. 28 | #[derive(Clone, Debug)] 29 | pub struct ConfigOption { 30 | /// The span over the keyword of the config option. 31 | pub span: Span, 32 | 33 | /// The config key-value pair. 34 | pub data: ConfigOptionData, 35 | } 36 | impl Parse for ConfigOption { 37 | fn parse(input: ParseStream) -> syn::Result { 38 | use ConfigOptionData as Data; 39 | use ConfigOptionKind as Kind; 40 | 41 | let span = input.span(); 42 | 43 | let kind = input.parse::()?; 44 | input.parse::()?; 45 | let data = match kind { 46 | Kind::Vis => Data::Vis(input.parse()?), 47 | Kind::RenameAll => Data::RenameAll(input.parse()?), 48 | Kind::Rename => Data::Rename(input.parse()?), 49 | Kind::Default => Data::Default(input.parse()?), 50 | Kind::Trim => Data::Trim(input.parse()?), 51 | }; 52 | 53 | Ok(Self { span, data }) 54 | } 55 | } 56 | 57 | /// All supported cases of `rename_all`. 58 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 59 | pub struct LitCase(convert_case::Case<'static>); 60 | impl Parse for LitCase { 61 | fn parse(input: ParseStream) -> syn::Result { 62 | use convert_case::Case as C; 63 | 64 | const SUPPORTED_CASES: [(&str, C); 8] = [ 65 | ("lowercase", C::Lower), 66 | ("UPPERCASE", C::Upper), 67 | ("PascalCase", C::Pascal), 68 | ("camelCase", C::Camel), 69 | ("snake_case", C::Snake), 70 | ("SCREAMING_SNAKE_CASE", C::UpperSnake), 71 | ("kebab-case", C::Kebab), 72 | ("SCREAMING-KEBAB-CASE", C::UpperKebab), 73 | ]; 74 | 75 | let arg = input.parse::()?; 76 | let Some(case) = SUPPORTED_CASES 77 | .into_iter() 78 | .find_map(|(name, case)| (name == arg.value()).then_some(case)) 79 | else { 80 | let options = SUPPORTED_CASES.map(|(name, _)| name).join(", "); 81 | Err(Error::new( 82 | arg.span(), 83 | format!("Case must be one of {options}."), 84 | ))? 85 | }; 86 | 87 | Ok(Self(case)) 88 | } 89 | } 90 | impl LitCase { 91 | pub fn value(&self) -> convert_case::Case<'static> { 92 | self.0 93 | } 94 | } 95 | 96 | /// The data of all known configuration options. 97 | #[derive(Clone, Debug, PartialEq, Eq, strum::EnumDiscriminants)] 98 | #[strum_discriminants( 99 | vis(pub(self)), 100 | name(ConfigOptionKind), 101 | derive(strum::Display, Hash), 102 | strum(serialize_all = "snake_case") 103 | )] 104 | pub enum ConfigOptionData { 105 | /// Custom visibility for the generated constant. 106 | /// 107 | /// E.g. `vis = pub(crate)`. 108 | Vis(Visibility), 109 | 110 | /// Custom casing of key names for the generated constants. 111 | /// 112 | /// E.g. `rename_all = "kebab-case"`. 113 | RenameAll(LitCase), 114 | 115 | /// Custom key name for the generated constant. 116 | /// 117 | /// E.g. `rename = "custom_field_name`, `rename = "CUSTOM_NAME_DOCS"`. 118 | Rename(LitStr), 119 | 120 | /// Use some default value when doc comments are absent. 121 | /// 122 | /// E.g. `default = "not documented"`. 123 | Default(Expr), 124 | 125 | /// Trim each line or not. 126 | /// 127 | /// E.g. `trim = false`. 128 | Trim(LitBool), 129 | } 130 | 131 | impl Parse for ConfigOptionKind { 132 | fn parse(input: ParseStream) -> syn::Result { 133 | let lookahead = input.lookahead1(); 134 | let ty = if lookahead.peek(kw::vis) { 135 | input.parse::()?; 136 | Self::Vis 137 | } else if lookahead.peek(kw::rename_all) { 138 | input.parse::()?; 139 | Self::RenameAll 140 | } else if lookahead.peek(kw::rename) { 141 | input.parse::()?; 142 | Self::Rename 143 | } else if lookahead.peek(kw::default) { 144 | input.parse::()?; 145 | Self::Default 146 | } else if lookahead.peek(kw::trim) { 147 | input.parse::()?; 148 | Self::Trim 149 | } else if lookahead.peek(kw::name) { 150 | Err(Error::new( 151 | input.span(), 152 | "`name` has been removed; use `rename` instead", 153 | ))? 154 | } else { 155 | Err(lookahead.error())? 156 | }; 157 | Ok(ty) 158 | } 159 | } 160 | 161 | /// Make sure there are no duplicate options. 162 | /// Otherwise produces an error with detailed span info. 163 | pub fn ensure_unique_options(opts: &[ConfigOption]) -> syn::Result<()> { 164 | for (kind, opts) in opts 165 | .iter() 166 | .into_group_map_by(|opt| ConfigOptionKind::from(&opt.data)) 167 | .into_iter() 168 | { 169 | match &opts[..] { 170 | [] => unreachable!(), // guaranteed by `into_group_map_by` 171 | [_unique] => continue, 172 | [first, rest @ ..] => { 173 | let initial_error = Error::new( 174 | first.span, 175 | format!("Option {kind} can only be declaration once"), 176 | ); 177 | let final_error = rest.iter().fold(initial_error, |mut err, opt| { 178 | err.combine(Error::new(opt.span, "Duplicate declaration here")); 179 | err 180 | }); 181 | Err(final_error)? 182 | } 183 | } 184 | } 185 | Ok(()) 186 | } 187 | 188 | /// Parse a list of attributes into a validated customisation. 189 | /// 190 | /// `impl TryFrom>` and using this function is preferred to 191 | /// `impl syn::parse::Parse` directly for situations where the options can come 192 | /// from multiple attributes and therefore multiple `MetaList`s. 193 | pub fn get_customisations_from_attrs(attrs: &[Attribute], attr_name: &str) -> syn::Result 194 | where 195 | T: TryFrom, Error = syn::Error>, 196 | { 197 | let options = attrs 198 | .iter() 199 | // remove irrelevant attributes 200 | .filter(|attr| attr.path().is_ident(attr_name)) 201 | // parse options 202 | .map(|attr| match &attr.meta { 203 | Meta::List(attr_inner) => { 204 | attr_inner.parse_args_with(Punctuated::::parse_terminated) 205 | } 206 | other_form => Err(syn::Error::new( 207 | other_form.span(), 208 | format!("{attr_name} is not list-like. Expecting `{attr_name}(...)`"), 209 | )), 210 | }) 211 | .collect::, _>>()? 212 | .into_iter() 213 | .flatten() 214 | .collect::>(); 215 | 216 | ensure_unique_options(&options)?; 217 | 218 | options.try_into() 219 | } 220 | -------------------------------------------------------------------------------- /documented-macros/src/config/derive.rs: -------------------------------------------------------------------------------- 1 | //! Generic configuration for derive macros. 2 | //! 3 | //! If a macro needs specialised configuration, this file can be used as a 4 | //! starting template. 5 | 6 | use syn::Expr; 7 | 8 | /// Configurable options for derive macros via helper attributes. 9 | /// 10 | /// Initial values are set to default. 11 | #[cfg_attr(feature = "customise", optfield::optfield( 12 | pub DeriveCustomisations, 13 | attrs = add(derive(Default)), 14 | merge_fn = pub apply_customisations, 15 | doc = "Parsed user-defined customisations of configurable options.\n\ 16 | \n\ 17 | Expected parse stream format: ` = , = , ...`" 18 | ))] 19 | #[derive(Clone, Debug, PartialEq, Eq)] 20 | pub struct DeriveConfig { 21 | // optfield does not rewrap `Option` by default, which is the desired behavior 22 | // see https://docs.rs/optfield/latest/optfield/#rewrapping-option-fields 23 | pub default_value: Option, 24 | pub trim: bool, 25 | } 26 | impl Default for DeriveConfig { 27 | fn default() -> Self { 28 | Self { default_value: None, trim: true } 29 | } 30 | } 31 | 32 | #[cfg(feature = "customise")] 33 | mod customise { 34 | use crate::config::{ 35 | customise_core::{ConfigOption, ConfigOptionData}, 36 | derive::{DeriveConfig, DeriveCustomisations}, 37 | }; 38 | 39 | impl DeriveConfig { 40 | /// Return a new instance of this config with customisations applied. 41 | pub fn with_customisations(&self, customisations: DeriveCustomisations) -> Self { 42 | let mut new = self.clone(); 43 | new.apply_customisations(customisations); 44 | new 45 | } 46 | } 47 | 48 | impl TryFrom> for DeriveCustomisations { 49 | type Error = syn::Error; 50 | 51 | /// Duplicate option rejection should be handled upstream. 52 | fn try_from(opts: Vec) -> Result { 53 | use ConfigOptionData as Data; 54 | 55 | let mut config = Self::default(); 56 | for opt in opts { 57 | match opt.data { 58 | Data::Vis(..) | Data::RenameAll(..) | Data::Rename(..) => Err( 59 | syn::Error::new(opt.span, "This config option is not applicable here"), 60 | )?, 61 | Data::Default(expr) => { 62 | config.default_value.replace(expr); 63 | } 64 | Data::Trim(trim) => { 65 | config.trim.replace(trim.value()); 66 | } 67 | } 68 | } 69 | Ok(config) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /documented-macros/src/config/derive_fields.rs: -------------------------------------------------------------------------------- 1 | //! Specialised configuration for `DocumentedFields` and `DocumentedFieldsOpt`. 2 | 3 | use convert_case::Case; 4 | use syn::Expr; 5 | 6 | /// Defines how to rename a particular field. 7 | #[derive(Clone, Debug, PartialEq, Eq)] 8 | #[allow(dead_code)] 9 | pub enum RenameMode { 10 | /// Use the original name, converted to another case. 11 | ToCase(Case<'static>), 12 | /// Use a custom name. 13 | Custom(String), 14 | } 15 | 16 | #[cfg_attr(feature = "customise", optfield::optfield( 17 | pub DeriveFieldsBaseCustomisations, 18 | attrs = (derive(Clone, Debug, Default, PartialEq, Eq)), 19 | merge_fn = pub apply_base_customisations, 20 | doc = "Parsed user-defined customisations of configurable options.\n\ 21 | Specialised variant for the type base of `DocumentedFields` and `DocumentedFieldsOpt`.\n\ 22 | \n\ 23 | Expected parse stream format: ` = , = , ...`" 24 | ))] 25 | #[cfg_attr(feature = "customise", optfield::optfield( 26 | pub DeriveFieldsCustomisations, 27 | attrs = (derive(Clone, Debug, Default, PartialEq, Eq)), 28 | merge_fn = pub apply_field_customisations, 29 | doc = "Parsed user-defined customisations of configurable options.\n\ 30 | Specialised variant for each field of `DocumentedFields` and `DocumentedFieldsOpt`.\n\ 31 | \n\ 32 | Expected parse stream format: ` = , = , ...`" 33 | ))] 34 | /// Configurable options for each field via helper attributes. 35 | /// 36 | /// Initial values are set to default. 37 | #[derive(Clone, Debug, PartialEq, Eq)] 38 | pub struct DeriveFieldsConfig { 39 | // optfield does not rewrap `Option` by default, which is the desired behavior 40 | // see https://docs.rs/optfield/latest/optfield/#rewrapping-option-fields 41 | pub rename_mode: Option, 42 | pub default_value: Option, 43 | pub trim: bool, 44 | } 45 | impl Default for DeriveFieldsConfig { 46 | fn default() -> Self { 47 | Self { 48 | rename_mode: None, 49 | default_value: None, 50 | trim: true, 51 | } 52 | } 53 | } 54 | 55 | #[cfg(feature = "customise")] 56 | mod customise { 57 | use crate::config::{ 58 | customise_core::{ConfigOption, ConfigOptionData}, 59 | derive_fields::{ 60 | DeriveFieldsBaseCustomisations, DeriveFieldsConfig, DeriveFieldsCustomisations, 61 | RenameMode, 62 | }, 63 | }; 64 | 65 | impl DeriveFieldsConfig { 66 | /// Return a new instance of this config with base customisations applied. 67 | pub fn with_base_customisations( 68 | &self, 69 | customisations: DeriveFieldsBaseCustomisations, 70 | ) -> Self { 71 | let mut new = self.clone(); 72 | new.apply_base_customisations(customisations); 73 | new 74 | } 75 | 76 | /// Return a new instance of this config with field customisations applied. 77 | pub fn with_field_customisations( 78 | &self, 79 | customisations: DeriveFieldsCustomisations, 80 | ) -> Self { 81 | let mut new = self.clone(); 82 | new.apply_field_customisations(customisations); 83 | new 84 | } 85 | } 86 | 87 | impl TryFrom> for DeriveFieldsBaseCustomisations { 88 | type Error = syn::Error; 89 | 90 | /// Duplicate option rejection should be handled upstream. 91 | fn try_from(opts: Vec) -> Result { 92 | use ConfigOptionData as Data; 93 | 94 | let mut config = Self::default(); 95 | for opt in opts { 96 | match opt.data { 97 | Data::Vis(..) | Data::Rename(..) => Err(syn::Error::new( 98 | opt.span, 99 | "This config option is not applicable here", 100 | ))?, 101 | Data::RenameAll(case) => { 102 | config.rename_mode.replace(RenameMode::ToCase(case.value())); 103 | } 104 | Data::Default(expr) => { 105 | config.default_value.replace(expr); 106 | } 107 | Data::Trim(trim) => { 108 | config.trim.replace(trim.value()); 109 | } 110 | } 111 | } 112 | Ok(config) 113 | } 114 | } 115 | 116 | impl TryFrom> for DeriveFieldsCustomisations { 117 | type Error = syn::Error; 118 | 119 | /// Duplicate option rejection should be handled upstream. 120 | fn try_from(opts: Vec) -> Result { 121 | use ConfigOptionData as Data; 122 | 123 | let mut config = Self::default(); 124 | for opt in opts { 125 | match opt.data { 126 | Data::Vis(..) => Err(syn::Error::new( 127 | opt.span, 128 | "This config option is not applicable here", 129 | ))?, 130 | Data::RenameAll(case) => { 131 | // `rename` always has priority over `rename_all` 132 | if !matches!(config.rename_mode, Some(RenameMode::Custom(_))) { 133 | config.rename_mode.replace(RenameMode::ToCase(case.value())); 134 | } 135 | } 136 | Data::Rename(name) => { 137 | config.rename_mode.replace(RenameMode::Custom(name.value())); 138 | } 139 | Data::Default(expr) => { 140 | config.default_value.replace(expr); 141 | } 142 | Data::Trim(trim) => { 143 | config.trim.replace(trim.value()); 144 | } 145 | } 146 | } 147 | Ok(config) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /documented-macros/src/derive_impl.rs: -------------------------------------------------------------------------------- 1 | //! Shared implementation for non-opt and opt variants of the derive macros. 2 | //! 3 | //! All functions in this module use the dependency injection pattern to 4 | //! generate the correct trait implementation for both macro variants. 5 | 6 | use convert_case::Casing; 7 | use proc_macro2::{Span, TokenStream}; 8 | use quote::{quote, ToTokens, TokenStreamExt}; 9 | use syn::{ 10 | spanned::Spanned, Data, DataEnum, DataStruct, DataUnion, DeriveInput, Error, Expr, Fields, 11 | Ident, 12 | }; 13 | 14 | #[cfg(feature = "customise")] 15 | use crate::config::customise_core::get_customisations_from_attrs; 16 | use crate::{ 17 | config::{ 18 | derive::DeriveConfig, 19 | derive_fields::{DeriveFieldsConfig, RenameMode}, 20 | }, 21 | util::{crate_module_path, get_docs}, 22 | }; 23 | 24 | /// The type of the doc comment. 25 | #[derive(Copy, Clone, Debug)] 26 | pub enum DocType { 27 | /// &'static str 28 | Str, 29 | /// Option<&'static str> 30 | OptStr, 31 | } 32 | impl ToTokens for DocType { 33 | fn to_tokens(&self, ts: &mut TokenStream) { 34 | let tokens = match self { 35 | Self::Str => quote! { &'static str }, 36 | Self::OptStr => quote! { Option<&'static str> }, 37 | }; 38 | ts.append_all([tokens]); 39 | } 40 | } 41 | impl DocType { 42 | /// Get the closure that determines how to handle optional docs. 43 | /// The closure takes three arguments: 44 | /// 45 | /// 1. The optional doc comments on an item 46 | /// 2. A default value optionally set by the user 47 | /// 3. The token span on which to report any errors 48 | /// 49 | /// And fallibly returns the tokenised doc comments. 50 | #[allow(clippy::type_complexity)] 51 | fn docs_handler_opt( 52 | &self, 53 | ) -> Box, Option, S) -> syn::Result> 54 | where 55 | S: ToTokens, 56 | { 57 | match self { 58 | Self::Str => Box::new( 59 | |docs_opt, default_opt, span| match (docs_opt, default_opt) { 60 | (Some(docs), _) => Ok(quote! { #docs }), 61 | (None, Some(default)) => Ok(quote! { #default }), 62 | (None, None) => Err(Error::new_spanned(span, "Missing doc comments")), 63 | }, 64 | ), 65 | Self::OptStr => Box::new(|docs_opt, default_opt, _span| { 66 | let tokens = match (docs_opt, default_opt) { 67 | (Some(docs), _) => quote! { Some(#docs) }, 68 | (None, Some(default)) => quote! { #default }, 69 | (None, None) => quote! { None }, 70 | }; 71 | Ok(tokens) 72 | }), 73 | } 74 | } 75 | 76 | /// Get the trait identifier, given a prefix. 77 | fn trait_ident_for(&self, prefix: &str) -> Ident { 78 | let name = match self { 79 | Self::Str => prefix.to_string(), 80 | Self::OptStr => format!("{prefix}Opt"), 81 | }; 82 | Ident::new(&name, Span::call_site()) 83 | } 84 | } 85 | 86 | /// Shared implementation of `Documented` & `DocumentedOpt`. 87 | pub fn documented_impl(input: DeriveInput, docs_ty: DocType) -> syn::Result { 88 | let trait_ident = docs_ty.trait_ident_for("Documented"); 89 | let ident = &input.ident; 90 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 91 | 92 | #[cfg(not(feature = "customise"))] 93 | let config = DeriveConfig::default(); 94 | #[cfg(feature = "customise")] 95 | let config = get_customisations_from_attrs(&input.attrs, "documented") 96 | .map(|c| DeriveConfig::default().with_customisations(c))?; 97 | 98 | let docs = get_docs(&input.attrs, config.trim) 99 | .and_then(|docs_opt| docs_ty.docs_handler_opt()(docs_opt, config.default_value, &input))?; 100 | 101 | Ok(quote! { 102 | #[automatically_derived] 103 | impl #impl_generics documented::#trait_ident for #ident #ty_generics #where_clause { 104 | const DOCS: #docs_ty = #docs; 105 | } 106 | }) 107 | } 108 | 109 | /// Shared implementation of `DocumentedFields` & `DocumentedFieldsOpt`. 110 | pub fn documented_fields_impl(input: DeriveInput, docs_ty: DocType) -> syn::Result { 111 | let trait_ident = docs_ty.trait_ident_for("DocumentedFields"); 112 | let ident = &input.ident; 113 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 114 | 115 | // `#[documented_fields(...)]` on container type 116 | #[cfg(not(feature = "customise"))] 117 | let base_config = DeriveFieldsConfig::default(); 118 | #[cfg(feature = "customise")] 119 | let base_config = get_customisations_from_attrs(&input.attrs, "documented_fields") 120 | .map(|c| DeriveFieldsConfig::default().with_base_customisations(c))?; 121 | 122 | let fields_attrs: Vec<_> = match input.data.clone() { 123 | Data::Enum(DataEnum { variants, .. }) => variants 124 | .into_iter() 125 | .map(|v| (v.to_token_stream(), Some(v.ident), v.attrs)) 126 | .collect(), 127 | Data::Struct(DataStruct { fields, .. }) => fields 128 | .into_iter() 129 | .map(|f| (f.to_token_stream(), f.ident, f.attrs)) 130 | .collect(), 131 | Data::Union(DataUnion { fields, .. }) => fields 132 | .named 133 | .into_iter() 134 | .map(|f| (f.to_token_stream(), f.ident, f.attrs)) 135 | .collect(), 136 | }; 137 | 138 | let (field_names, field_docs) = fields_attrs 139 | .into_iter() 140 | .map(|(span, ident, attrs)| { 141 | #[cfg(not(feature = "customise"))] 142 | let config = base_config.clone(); 143 | #[cfg(feature = "customise")] 144 | let config = get_customisations_from_attrs(&attrs, "documented_fields") 145 | .map(|c| base_config.with_field_customisations(c))?; 146 | let name = match config.rename_mode { 147 | None => ident.map(|ident| ident.to_string()), 148 | Some(RenameMode::ToCase(case)) => { 149 | ident.map(|ident| ident.to_string().to_case(case)) 150 | } 151 | Some(RenameMode::Custom(name)) => Some(name), 152 | }; 153 | get_docs(&attrs, config.trim) 154 | .and_then(|docs_opt| { 155 | docs_ty.docs_handler_opt()(docs_opt, config.default_value, span) 156 | }) 157 | .map(|docs| (name, docs)) 158 | }) 159 | .collect::>>()? 160 | .into_iter() 161 | .unzip::<_, _, Vec<_>, Vec<_>>(); 162 | 163 | let (field_names, phf_match_arms) = field_names 164 | .into_iter() 165 | .enumerate() 166 | .filter_map(|(i, field)| field.map(|field| (i, field.as_str().to_owned()))) 167 | .map(|(i, name)| (name.clone(), quote! { #name => #i, })) 168 | .unzip::<_, _, Vec<_>, Vec<_>>(); 169 | 170 | let documented_module_path = crate_module_path(); 171 | 172 | Ok(quote! { 173 | #[automatically_derived] 174 | impl #impl_generics documented::#trait_ident for #ident #ty_generics #where_clause { 175 | const FIELD_NAMES: &'static [&'static str] = &[#(#field_names),*]; 176 | const FIELD_DOCS: &'static [#docs_ty] = &[#(#field_docs),*]; 177 | 178 | fn __documented_get_index<__Documented_T: AsRef>(field_name: __Documented_T) -> Option { 179 | use #documented_module_path::_private_phf_reexport_for_macro as phf; 180 | 181 | static PHF: phf::Map<&'static str, usize> = phf::phf_map! { 182 | #(#phf_match_arms)* 183 | }; 184 | PHF.get(field_name.as_ref()).copied() 185 | } 186 | } 187 | }) 188 | } 189 | 190 | /// Shared implementation of `DocumentedVariants` & `DocumentedVariantsOpt`. 191 | pub fn documented_variants_impl(input: DeriveInput, docs_ty: DocType) -> syn::Result { 192 | let trait_ident = docs_ty.trait_ident_for("DocumentedVariants"); 193 | let ident = &input.ident; 194 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 195 | 196 | // `#[documented_variants(...)]` on container type 197 | #[cfg(not(feature = "customise"))] 198 | let base_config = DeriveConfig::default(); 199 | #[cfg(feature = "customise")] 200 | let base_config = get_customisations_from_attrs(&input.attrs, "documented_variants") 201 | .map(|c| DeriveConfig::default().with_customisations(c))?; 202 | 203 | let variants = match input.data { 204 | Data::Enum(DataEnum { variants, .. }) => Ok(variants), 205 | Data::Struct(DataStruct { struct_token, .. }) => Err(struct_token.span()), 206 | Data::Union(DataUnion { union_token, .. }) => Err(union_token.span()), 207 | } 208 | .map_err(|span| { 209 | Error::new( 210 | span, 211 | "DocumentedVariants can only be used on enums.\n\ 212 | For structs and unions, use DocumentedFields instead.", 213 | ) 214 | })?; 215 | 216 | let variants_docs = variants 217 | .into_iter() 218 | .map(|v| { 219 | #[cfg(not(feature = "customise"))] 220 | let config = base_config.clone(); 221 | #[cfg(feature = "customise")] 222 | let config = get_customisations_from_attrs(&v.attrs, "documented_variants") 223 | .map(|c| base_config.with_customisations(c))?; 224 | get_docs(&v.attrs, config.trim) 225 | .and_then(|docs_opt| docs_ty.docs_handler_opt()(docs_opt, config.default_value, &v)) 226 | .map(|docs| (v.ident, v.fields, docs)) 227 | }) 228 | .collect::>>()?; 229 | 230 | let match_arms = variants_docs 231 | .into_iter() 232 | .map(|(ident, fields, docs)| { 233 | let pat = match fields { 234 | Fields::Unit => quote! { Self::#ident }, 235 | Fields::Unnamed(_) => quote! { Self::#ident(..) }, 236 | Fields::Named(_) => quote! { Self::#ident{..} }, 237 | }; 238 | quote! { #pat => #docs, } 239 | }) 240 | .collect::>(); 241 | 242 | // IDEA: I'd like to use phf here, but it doesn't seem to be possible at the moment, 243 | // because there isn't a way to get an enum's discriminant at compile time 244 | // if this becomes possible in the future, or alternatively you have a good workaround, 245 | // improvement suggestions are more than welcomed 246 | Ok(quote! { 247 | #[automatically_derived] 248 | impl #impl_generics documented::#trait_ident for #ident #ty_generics #where_clause { 249 | fn get_variant_docs(&self) -> #docs_ty { 250 | match self { 251 | #(#match_arms)* 252 | } 253 | } 254 | } 255 | }) 256 | } 257 | -------------------------------------------------------------------------------- /documented-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod attr_impl; 2 | mod config; 3 | mod derive_impl; 4 | pub(crate) mod util; 5 | 6 | use proc_macro::TokenStream; 7 | use syn::{parse_macro_input, Error}; 8 | 9 | use crate::{ 10 | attr_impl::docs_const_impl, 11 | derive_impl::{documented_fields_impl, documented_impl, documented_variants_impl, DocType}, 12 | }; 13 | 14 | /// Derive proc-macro for `Documented` trait. 15 | /// 16 | /// # Example 17 | /// 18 | /// ```rust 19 | /// use documented::Documented; 20 | /// 21 | /// /// Nice. 22 | /// /// Multiple single-line doc comments are supported. 23 | /// /// 24 | /// /** Multi-line doc comments are supported too. 25 | /// Each line of the multi-line block is individually trimmed by default. 26 | /// Note the lack of spaces in front of this line. 27 | /// */ 28 | /// #[doc = "Attribute-style documentation is supported too."] 29 | /// #[derive(Documented)] 30 | /// struct BornIn69; 31 | /// 32 | /// let doc_str = "Nice. 33 | /// Multiple single-line doc comments are supported. 34 | /// 35 | /// Multi-line doc comments are supported too. 36 | /// Each line of the multi-line block is individually trimmed by default. 37 | /// Note the lack of spaces in front of this line. 38 | /// 39 | /// Attribute-style documentation is supported too."; 40 | /// assert_eq!(BornIn69::DOCS, doc_str); 41 | /// ``` 42 | /// 43 | /// # Configuration 44 | /// 45 | /// With the `customise` feature enabled, you can customise this macro's 46 | /// behaviour using the `#[documented(...)]` attribute. 47 | /// 48 | /// Currently, you can: 49 | /// 50 | /// ## 1. set a default value when doc comments are absent like so: 51 | /// 52 | /// ```rust 53 | /// # use documented::Documented; 54 | /// #[derive(Documented)] 55 | /// #[documented(default = "The answer is fries.")] 56 | /// struct WhosTurnIsIt; 57 | /// 58 | /// assert_eq!(WhosTurnIsIt::DOCS, "The answer is fries."); 59 | /// ``` 60 | /// 61 | /// This option is primarily designed for [`DocumentedFields`] and 62 | /// [`DocumentedVariants`], so it's probably not very useful here. But it could 63 | /// conceivably come in handy in some niche meta-programming contexts. 64 | /// 65 | /// ## 2. disable line-trimming like so: 66 | /// 67 | /// ```rust 68 | /// # use documented::Documented; 69 | /// /// Terrible. 70 | /// #[derive(Documented)] 71 | /// #[documented(trim = false)] 72 | /// struct Frankly; 73 | /// 74 | /// assert_eq!(Frankly::DOCS, " Terrible."); 75 | /// ``` 76 | /// 77 | /// If there are other configuration options you wish to have, please submit an 78 | /// issue or a PR. 79 | #[cfg_attr(not(feature = "customise"), proc_macro_derive(Documented))] 80 | #[cfg_attr( 81 | feature = "customise", 82 | proc_macro_derive(Documented, attributes(documented)) 83 | )] 84 | pub fn documented(input: TokenStream) -> TokenStream { 85 | documented_impl(parse_macro_input!(input), DocType::Str) 86 | .unwrap_or_else(Error::into_compile_error) 87 | .into() 88 | } 89 | 90 | /// Derive proc-macro for `DocumentedOpt` trait. 91 | /// 92 | /// See [`Documented`] for usage. 93 | #[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedOpt))] 94 | #[cfg_attr( 95 | feature = "customise", 96 | proc_macro_derive(DocumentedOpt, attributes(documented)) 97 | )] 98 | pub fn documented_opt(input: TokenStream) -> TokenStream { 99 | documented_impl(parse_macro_input!(input), DocType::OptStr) 100 | .unwrap_or_else(Error::into_compile_error) 101 | .into() 102 | } 103 | 104 | /// Derive proc-macro for `DocumentedFields` trait. 105 | /// 106 | /// # Example 107 | /// 108 | /// ```rust 109 | /// use documented::DocumentedFields; 110 | /// 111 | /// #[derive(DocumentedFields)] 112 | /// struct BornIn69 { 113 | /// /// Cry like a grandmaster. 114 | /// rawr: String, 115 | /// /// Before what? 116 | /// explosive: usize, 117 | /// }; 118 | /// 119 | /// assert_eq!( 120 | /// BornIn69::FIELD_DOCS, 121 | /// ["Cry like a grandmaster.", "Before what?"] 122 | /// ); 123 | /// ``` 124 | /// 125 | /// You can also use `DocumentedFields::get_field_docs` to access the fields' 126 | /// documentation using their names. 127 | /// 128 | /// ```rust 129 | /// # use documented::{DocumentedFields, Error}; 130 | /// # 131 | /// # #[derive(DocumentedFields)] 132 | /// # struct BornIn69 { 133 | /// # /// Cry like a grandmaster. 134 | /// # rawr: String, 135 | /// # /// Before what? 136 | /// # explosive: usize, 137 | /// # }; 138 | /// # 139 | /// assert_eq!( 140 | /// BornIn69::get_field_docs("rawr"), 141 | /// Ok("Cry like a grandmaster.") 142 | /// ); 143 | /// assert_eq!(BornIn69::get_field_docs("explosive"), Ok("Before what?")); 144 | /// assert_eq!( 145 | /// BornIn69::get_field_docs("gotcha"), 146 | /// Err(Error::NoSuchField("gotcha".to_string())) 147 | /// ); 148 | /// ``` 149 | /// 150 | /// # Configuration 151 | /// 152 | /// With the `customise` feature enabled, you can customise this macro's 153 | /// behaviour using the `#[documented_fields(...)]` attribute. Note that this 154 | /// attribute works on both the container and each individual field, with the 155 | /// per-field configurations overriding container configurations, which 156 | /// override the default. 157 | /// 158 | /// Currently, you can: 159 | /// 160 | /// ## 1. set a different case convention for `get_field_docs` like so: 161 | /// 162 | /// ```rust 163 | /// # use documented::DocumentedFields; 164 | /// #[derive(DocumentedFields)] 165 | /// #[documented_fields(rename_all = "kebab-case")] 166 | /// struct BooksYouShouldWrite { 167 | /// /// It's my move? 168 | /// whose_turn_is_it: String, 169 | /// /// Isn't it checkmate? 170 | /// #[documented_fields(rename_all = "PascalCase")] 171 | /// how_many_legal_moves_do_you_have: String, 172 | /// } 173 | /// 174 | /// assert_eq!( 175 | /// BooksYouShouldWrite::get_field_docs("whose-turn-is-it"), 176 | /// Ok("It's my move?") 177 | /// ); 178 | /// assert_eq!( 179 | /// BooksYouShouldWrite::get_field_docs("HowManyLegalMovesDoYouHave"), 180 | /// Ok("Isn't it checkmate?") 181 | /// ); 182 | /// ``` 183 | /// 184 | /// ## 2. set a custom name for a specific field for `get_field_docs` like so: 185 | /// 186 | /// ```rust 187 | /// # use documented::DocumentedFields; 188 | /// #[derive(DocumentedFields)] 189 | /// // #[documented_field(rename = "fries")] // this is not allowed 190 | /// struct ThisPosition { 191 | /// /// I'm guessing, but I'm not really guessing. 192 | /// #[documented_fields(rename = "knows")] 193 | /// esserman_knows: bool, 194 | /// /// And that's true if you're van Wely. 195 | /// #[documented_fields(rename = "doesnt_know")] 196 | /// van_wely_doesnt: bool, 197 | /// } 198 | /// 199 | /// assert_eq!( 200 | /// ThisPosition::get_field_docs("knows"), 201 | /// Ok("I'm guessing, but I'm not really guessing.") 202 | /// ); 203 | /// assert_eq!( 204 | /// ThisPosition::get_field_docs("doesnt_know"), 205 | /// Ok("And that's true if you're van Wely.") 206 | /// ); 207 | /// ``` 208 | /// 209 | /// Obviously this option is only available on each individual field. 210 | /// It makes no sense on the container. 211 | /// 212 | /// This option also always takes priority over `rename_all`. 213 | /// 214 | /// ## 3. set a default value when doc comments are absent like so: 215 | /// 216 | /// ```rust 217 | /// # use documented::DocumentedFields; 218 | /// #[derive(DocumentedFields)] 219 | /// #[documented_fields(default = "Confusing the audience.")] 220 | /// struct SettingUpForTheNextGame { 221 | /// rh8: bool, 222 | /// ng8: bool, 223 | /// /// Always play: 224 | /// bf8: bool, 225 | /// } 226 | /// 227 | /// assert_eq!( 228 | /// SettingUpForTheNextGame::FIELD_DOCS, 229 | /// [ 230 | /// "Confusing the audience.", 231 | /// "Confusing the audience.", 232 | /// "Always play:" 233 | /// ] 234 | /// ); 235 | /// ``` 236 | /// 237 | /// ## 4. (selectively) disable line-trimming like so: 238 | /// 239 | /// ```rust 240 | /// # use documented::DocumentedFields; 241 | /// #[derive(DocumentedFields)] 242 | /// #[documented_fields(trim = false)] 243 | /// struct Frankly { 244 | /// /// Delicious. 245 | /// perrier: usize, 246 | /// /// I'm vegan. 247 | /// #[documented_fields(trim = true)] 248 | /// fried_liver: bool, 249 | /// } 250 | /// 251 | /// assert_eq!(Frankly::FIELD_DOCS, [" Delicious.", "I'm vegan."]); 252 | /// ``` 253 | /// 254 | /// If there are other configuration options you wish to have, please 255 | /// submit an issue or a PR. 256 | #[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedFields))] 257 | #[cfg_attr( 258 | feature = "customise", 259 | proc_macro_derive(DocumentedFields, attributes(documented_fields)) 260 | )] 261 | pub fn documented_fields(input: TokenStream) -> TokenStream { 262 | documented_fields_impl(parse_macro_input!(input), DocType::Str) 263 | .unwrap_or_else(Error::into_compile_error) 264 | .into() 265 | } 266 | 267 | /// Derive proc-macro for `DocumentedFieldsOpt` trait. 268 | /// 269 | /// See [`DocumentedFields`] for usage. 270 | #[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedFieldsOpt))] 271 | #[cfg_attr( 272 | feature = "customise", 273 | proc_macro_derive(DocumentedFieldsOpt, attributes(documented_fields)) 274 | )] 275 | pub fn documented_fields_opt(input: TokenStream) -> TokenStream { 276 | documented_fields_impl(parse_macro_input!(input), DocType::OptStr) 277 | .unwrap_or_else(Error::into_compile_error) 278 | .into() 279 | } 280 | 281 | /// Derive proc-macro for `DocumentedVariants` trait. 282 | /// 283 | /// # Example 284 | /// 285 | /// ```rust 286 | /// use documented::{DocumentedVariants, Error}; 287 | /// 288 | /// #[derive(DocumentedVariants)] 289 | /// enum NeverPlay { 290 | /// /// Terrible. 291 | /// F3, 292 | /// /// I fell out of my chair. 293 | /// F6, 294 | /// } 295 | /// 296 | /// assert_eq!(NeverPlay::F3.get_variant_docs(), "Terrible."); 297 | /// assert_eq!(NeverPlay::F6.get_variant_docs(), "I fell out of my chair."); 298 | /// ``` 299 | /// 300 | /// # Configuration 301 | /// 302 | /// With the `customise` feature enabled, you can customise this macro's 303 | /// behaviour using the `#[documented_variants(...)]` attribute. Note that this 304 | /// attribute works on both the container and each individual variant, with the 305 | /// per-variant configurations overriding container configurations, which 306 | /// override the default. 307 | /// 308 | /// Currently, you can: 309 | /// 310 | /// ## 1. set a default value when doc comments are absent like so: 311 | /// 312 | /// ```rust 313 | /// # use documented::DocumentedVariants; 314 | /// #[derive(DocumentedVariants)] 315 | /// #[documented_variants(default = "Still theory.")] 316 | /// enum OurHeroPlayed { 317 | /// G4Mate, 318 | /// OOOMate, 319 | /// /// Frankly ridiculous. 320 | /// Bf1g2Mate, 321 | /// } 322 | /// 323 | /// assert_eq!(OurHeroPlayed::G4Mate.get_variant_docs(), "Still theory."); 324 | /// assert_eq!(OurHeroPlayed::OOOMate.get_variant_docs(), "Still theory."); 325 | /// assert_eq!( 326 | /// OurHeroPlayed::Bf1g2Mate.get_variant_docs(), 327 | /// "Frankly ridiculous." 328 | /// ); 329 | /// ``` 330 | /// 331 | /// ## 2. (selectively) disable line-trimming like so: 332 | /// 333 | /// ```rust 334 | /// # use documented::DocumentedVariants; 335 | /// #[derive(DocumentedVariants)] 336 | /// #[documented_variants(trim = false)] 337 | /// enum Always { 338 | /// /// Or the quality. 339 | /// SacTheExchange, 340 | /// /// Like a Frenchman. 341 | /// #[documented_variants(trim = true)] 342 | /// Retreat, 343 | /// } 344 | /// assert_eq!( 345 | /// Always::SacTheExchange.get_variant_docs(), 346 | /// " Or the quality." 347 | /// ); 348 | /// assert_eq!(Always::Retreat.get_variant_docs(), "Like a Frenchman."); 349 | /// ``` 350 | /// 351 | /// If there are other configuration options you wish to have, please 352 | /// submit an issue or a PR. 353 | #[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedVariants))] 354 | #[cfg_attr( 355 | feature = "customise", 356 | proc_macro_derive(DocumentedVariants, attributes(documented_variants)) 357 | )] 358 | pub fn documented_variants(input: TokenStream) -> TokenStream { 359 | documented_variants_impl(parse_macro_input!(input), DocType::Str) 360 | .unwrap_or_else(Error::into_compile_error) 361 | .into() 362 | } 363 | 364 | /// Derive proc-macro for `DocumentedVariantsOpt` trait. 365 | /// 366 | /// See [`DocumentedVariants`] for usage. 367 | #[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedVariantsOpt))] 368 | #[cfg_attr( 369 | feature = "customise", 370 | proc_macro_derive(DocumentedVariantsOpt, attributes(documented_variants)) 371 | )] 372 | pub fn documented_variants_opt(input: TokenStream) -> TokenStream { 373 | documented_variants_impl(parse_macro_input!(input), DocType::OptStr) 374 | .unwrap_or_else(Error::into_compile_error) 375 | .into() 376 | } 377 | 378 | /// Macro to extract the documentation on any item that accepts doc comments 379 | /// and store it in a const variable. 380 | /// 381 | /// By default, this const variable inherits visibility from its parent item. 382 | /// This can be manually configured; see configuration section below. 383 | /// 384 | /// # Examples 385 | /// 386 | /// ```rust 387 | /// use documented::docs_const; 388 | /// 389 | /// /// This is a test function 390 | /// #[docs_const] 391 | /// fn test_fn() {} 392 | /// 393 | /// assert_eq!(TEST_FN_DOCS, "This is a test function"); 394 | /// ``` 395 | /// 396 | /// # Configuration 397 | /// 398 | /// With the `customise` feature enabled, you can customise this macro's 399 | /// behaviour using attribute arguments. 400 | /// 401 | /// Currently, you can: 402 | /// 403 | /// ## 1. set a custom constant visibility like so: 404 | /// 405 | /// ```rust 406 | /// mod submodule { 407 | /// # use documented::docs_const; 408 | /// /// Boo! 409 | /// #[docs_const(vis = pub)] 410 | /// struct Wooooo; 411 | /// } 412 | /// 413 | /// // notice how the constant can be seen from outside 414 | /// assert_eq!(submodule::WOOOOO_DOCS, "Boo!"); 415 | /// ``` 416 | /// 417 | /// ## 2. set a custom constant name like so: 418 | /// 419 | /// ```rust 420 | /// # use documented::docs_const; 421 | /// /// If you have a question raise your hand 422 | /// #[docs_const(rename = "DONT_RAISE_YOUR_HAND")] 423 | /// mod whatever {} 424 | /// 425 | /// assert_eq!(DONT_RAISE_YOUR_HAND, "If you have a question raise your hand"); 426 | /// ``` 427 | /// 428 | /// ## 3. set a default value when doc comments are absent like so: 429 | /// 430 | /// ```rust 431 | /// use documented::docs_const; 432 | /// 433 | /// #[docs_const(default = "In this position many of you blunder.")] 434 | /// trait StartingPosition {} 435 | /// 436 | /// assert_eq!( 437 | /// STARTING_POSITION_DOCS, 438 | /// "In this position many of you blunder." 439 | /// ); 440 | /// ``` 441 | /// 442 | /// This option is primarily designed for [`DocumentedFields`] and 443 | /// [`DocumentedVariants`], so it's probably not very useful here. But it could 444 | /// conceivably come in handy in some niche meta-programming contexts. 445 | /// 446 | /// ## 4. disable line-trimming like so: 447 | /// 448 | /// ```rust 449 | /// # use documented::docs_const; 450 | /// /// This is a test constant 451 | /// #[docs_const(trim = false)] 452 | /// const test_const: u8 = 0; 453 | /// 454 | /// assert_eq!(TEST_CONST_DOCS, " This is a test constant"); 455 | /// ``` 456 | /// 457 | /// --- 458 | /// 459 | /// Multiple option can be specified in a list like so: 460 | /// `name = "FOO", trim = false`. 461 | /// 462 | /// If there are other configuration options you wish to have, please 463 | /// submit an issue or a PR. 464 | #[proc_macro_attribute] 465 | pub fn docs_const(#[allow(unused_variables)] attr: TokenStream, item: TokenStream) -> TokenStream { 466 | #[cfg(not(feature = "customise"))] 467 | let ts = docs_const_impl(parse_macro_input!(item)); 468 | #[cfg(feature = "customise")] 469 | let ts = docs_const_impl(parse_macro_input!(item), parse_macro_input!(attr)); 470 | 471 | ts.unwrap_or_else(Error::into_compile_error).into() 472 | } 473 | -------------------------------------------------------------------------------- /documented-macros/src/util.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | parse_quote, spanned::Spanned, Attribute, Error, Expr, ExprLit, Item, Lit, Meta, Path, 3 | Visibility, 4 | }; 5 | 6 | pub fn crate_module_path() -> Path { 7 | parse_quote!(::documented) 8 | } 9 | 10 | pub fn get_vis_name_attrs(item: &Item) -> syn::Result<(Visibility, String, &[Attribute])> { 11 | match item { 12 | Item::Const(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 13 | Item::Enum(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 14 | Item::ExternCrate(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 15 | Item::Fn(item) => Ok((item.vis.clone(), item.sig.ident.to_string(), &item.attrs)), 16 | Item::Mod(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 17 | Item::Static(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 18 | Item::Struct(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 19 | Item::Trait(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 20 | Item::TraitAlias(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 21 | Item::Type(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 22 | Item::Union(item) => Ok((item.vis.clone(), item.ident.to_string(), &item.attrs)), 23 | Item::Macro(item) => { 24 | let Some(ref ident) = item.ident else { 25 | Err(Error::new( 26 | item.span(), 27 | "Doc comments are not supported on macro invocations", 28 | ))? 29 | }; 30 | Ok((Visibility::Inherited, ident.to_string(), &item.attrs)) 31 | } 32 | Item::ForeignMod(_) | Item::Impl(_) | Item::Use(_) => Err(Error::new( 33 | item.span(), 34 | "Doc comments are not supported on this item", 35 | )), 36 | Item::Verbatim(_) => Err(Error::new( 37 | item.span(), 38 | "Doc comments are not supported on items unknown to syn", 39 | )), 40 | _ => Err(Error::new( 41 | item.span(), 42 | "This item is unknown to documented\n\ 43 | If this item supports doc comments, consider submitting an issue or PR", 44 | )), 45 | } 46 | } 47 | 48 | pub fn get_docs(attrs: &[Attribute], trim: bool) -> syn::Result> { 49 | let string_literals = attrs 50 | .iter() 51 | .filter_map(|attr| match attr.meta { 52 | Meta::NameValue(ref name_value) if name_value.path.is_ident("doc") => { 53 | Some(&name_value.value) 54 | } 55 | _ => None, 56 | }) 57 | .map(|expr| match expr { 58 | Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => Ok(s.value()), 59 | other => Err(Error::new( 60 | other.span(), 61 | "Doc comment is not a string literal", 62 | )), 63 | }) 64 | .collect::, _>>()?; 65 | 66 | if string_literals.is_empty() { 67 | return Ok(None); 68 | } 69 | 70 | let docs = if trim { 71 | string_literals 72 | .iter() 73 | .flat_map(|lit| lit.split('\n').collect::>()) 74 | .map(|line| line.trim().to_string()) 75 | .collect::>() 76 | .join("\n") 77 | } else { 78 | string_literals.join("\n") 79 | }; 80 | 81 | Ok(Some(docs)) 82 | } 83 | -------------------------------------------------------------------------------- /documented-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "documented-test" 4 | publish = false 5 | rust-version.workspace = true 6 | version.workspace = true 7 | 8 | [dev-dependencies] 9 | documented = { path = "../lib" } 10 | 11 | [features] 12 | customise = ["documented/customise"] 13 | default = ["customise"] 14 | -------------------------------------------------------------------------------- /documented-test/src/attr.rs: -------------------------------------------------------------------------------- 1 | mod docs_const; 2 | -------------------------------------------------------------------------------- /documented-test/src/attr/docs_const.rs: -------------------------------------------------------------------------------- 1 | use documented::docs_const; 2 | 3 | #[test] 4 | fn it_works() { 5 | /// This is a test function 6 | #[docs_const] 7 | #[allow(dead_code)] 8 | fn test_fn() {} 9 | 10 | assert_eq!(TEST_FN_DOCS, "This is a test function"); 11 | } 12 | 13 | // Note: I found no way to test whether the visibility of the item is preserved. 14 | // Manual testing showed that it is preserved, but I couldn't find a way to test it in code. 15 | 16 | #[test] 17 | fn multiple_docs_work() { 18 | /// This is a test function 19 | /** This is the second line of the doc*/ 20 | #[doc = "This is the third line of the doc"] 21 | #[docs_const] 22 | #[allow(dead_code)] 23 | fn test_fn() {} 24 | 25 | assert_eq!(TEST_FN_DOCS, "This is a test function\nThis is the second line of the doc\nThis is the third line of the doc"); 26 | } 27 | 28 | #[cfg(feature = "customise")] 29 | mod test_customise { 30 | use documented::docs_const; 31 | 32 | #[test] 33 | fn custom_visibility_works() { 34 | mod class { 35 | use documented::docs_const; 36 | 37 | #[docs_const(vis = pub)] 38 | #[allow(dead_code)] 39 | /// Arjun! 40 | trait RandomStudent {} 41 | } 42 | 43 | assert_eq!(class::RANDOM_STUDENT_DOCS, "Arjun!"); 44 | } 45 | 46 | #[test] 47 | fn rename_works() { 48 | /// Suspicious 49 | #[docs_const(rename = "NEVER_PLAY_F6")] 50 | #[allow(dead_code)] 51 | mod f6 {} 52 | 53 | assert_eq!(NEVER_PLAY_F6, "Suspicious"); 54 | } 55 | 56 | #[test] 57 | fn trim_works() { 58 | /// This is a test function 59 | /// Test Trim 60 | #[docs_const(trim = true)] // technically redundant, as it's the default 61 | #[allow(dead_code)] 62 | fn test_fn() {} 63 | 64 | assert_eq!(TEST_FN_DOCS, "This is a test function\nTest Trim"); 65 | } 66 | 67 | #[test] 68 | fn no_trim_works() { 69 | /// This is a test function 70 | /// Test Trim 71 | #[docs_const(trim = false)] 72 | #[allow(dead_code)] 73 | fn test_fn() {} 74 | 75 | assert_eq!( 76 | TEST_FN_DOCS, 77 | " This is a test function \n Test Trim" 78 | ); // The whitespace is preserved, even on the end of the first line 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /documented-test/src/derive.rs: -------------------------------------------------------------------------------- 1 | //! Tests for derive macros. 2 | //! 3 | //! The tests for `*Opt` macros are deliberately incomplete, because they share 4 | //! the vast majority of their implementation with their respective non-opt 5 | //! counterparts. 6 | 7 | mod documented; 8 | mod documented_fields; 9 | mod documented_fields_opt; 10 | mod documented_opt; 11 | mod documented_variants; 12 | mod documented_variants_opt; 13 | -------------------------------------------------------------------------------- /documented-test/src/derive/documented.rs: -------------------------------------------------------------------------------- 1 | mod test_use { 2 | use documented::Documented; 3 | 4 | #[test] 5 | fn it_works() { 6 | /// 69 7 | #[derive(Documented)] 8 | struct Nice; 9 | 10 | assert_eq!(Nice::DOCS, "69"); 11 | } 12 | 13 | #[test] 14 | fn multi_line_works() { 15 | /// 69 16 | /// 420 17 | /// 18 | /// 1337 19 | #[derive(Documented)] 20 | struct Nice; 21 | 22 | let docs = "69\n420\n\n1337"; 23 | assert_eq!(Nice::DOCS, docs); 24 | } 25 | 26 | #[test] 27 | fn every_style_works() { 28 | /// 69 29 | /** Very nice 30 | 420 */ 31 | #[doc = "1337"] 32 | #[derive(Documented)] 33 | struct Nicer; 34 | 35 | let docs = "69\nVery nice\n420\n1337"; 36 | assert_eq!(Nicer::DOCS, docs); 37 | } 38 | 39 | #[test] 40 | fn generic_type_works() { 41 | /// Wow 42 | #[allow(dead_code)] 43 | #[derive(Documented)] 44 | struct Doge { 45 | much: T, 46 | } 47 | 48 | assert_eq!(Doge::::DOCS, "Wow"); 49 | } 50 | 51 | #[test] 52 | fn generic_type_with_bounds_works() { 53 | /// Wow 54 | #[allow(dead_code)] 55 | #[derive(Documented)] 56 | struct Doge { 57 | much: T, 58 | } 59 | 60 | assert_eq!(Doge::::DOCS, "Wow"); 61 | } 62 | 63 | #[test] 64 | fn const_generic_type_works() { 65 | /// Wow 66 | #[allow(dead_code)] 67 | #[derive(Documented)] 68 | struct Doge { 69 | much: [u8; LEN], 70 | } 71 | 72 | assert_eq!(Doge::<69>::DOCS, "Wow"); 73 | } 74 | 75 | #[test] 76 | fn lifetimed_type_works() { 77 | /// Wow 78 | #[allow(dead_code)] 79 | #[derive(Documented)] 80 | struct Doge<'a> { 81 | much: &'a str, 82 | } 83 | 84 | assert_eq!(Doge::DOCS, "Wow"); 85 | } 86 | } 87 | 88 | mod test_qualified { 89 | #[test] 90 | fn it_works() { 91 | /// 69 92 | #[derive(documented::Documented)] 93 | struct Nice; 94 | 95 | assert_eq!(::DOCS, "69"); 96 | } 97 | } 98 | 99 | #[cfg(feature = "customise")] 100 | mod test_customise { 101 | use documented::Documented; 102 | 103 | #[test] 104 | fn empty_customise_works() { 105 | /** Wow 106 | much 107 | doge 108 | */ 109 | #[derive(Documented)] 110 | #[documented()] 111 | struct Doge; 112 | 113 | let doc_str = "Wow 114 | much 115 | doge 116 | "; 117 | assert_eq!(Doge::DOCS, doc_str); 118 | } 119 | 120 | #[test] 121 | fn multiple_attrs_works() { 122 | /** Wow 123 | much 124 | doge 125 | */ 126 | #[derive(Documented)] 127 | #[documented()] 128 | #[documented()] 129 | struct Doge; 130 | 131 | let doc_str = "Wow 132 | much 133 | doge 134 | "; 135 | assert_eq!(Doge::DOCS, doc_str); 136 | } 137 | 138 | #[test] 139 | fn default_works_with_literal() { 140 | #[derive(Documented)] 141 | #[documented(default = "3 goals 2 assists!")] 142 | struct Age37; 143 | 144 | assert_eq!(Age37::DOCS, "3 goals 2 assists!"); 145 | } 146 | 147 | #[test] 148 | fn default_works_with_const() { 149 | const DOC_STR: &str = "3 goals 2 assists!"; 150 | 151 | #[derive(Documented)] 152 | #[documented(default = DOC_STR)] 153 | struct Age37; 154 | 155 | assert_eq!(Age37::DOCS, DOC_STR); 156 | } 157 | 158 | #[test] 159 | fn default_works_with_macros() { 160 | #[derive(Documented)] 161 | #[documented(default = concat!("3 goals ", "2 assists!"))] 162 | struct Age37; 163 | 164 | assert_eq!(Age37::DOCS, "3 goals 2 assists!"); 165 | } 166 | 167 | #[test] 168 | fn trim_false_works() { 169 | /** Wow 170 | much 171 | doge 172 | */ 173 | #[derive(Documented)] 174 | #[documented(trim = false)] 175 | struct Doge; 176 | 177 | let doc_str = " Wow 178 | much 179 | doge 180 | "; 181 | assert_eq!(Doge::DOCS, doc_str); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /documented-test/src/derive/documented_fields.rs: -------------------------------------------------------------------------------- 1 | use documented::{DocumentedFields, Error}; 2 | 3 | #[test] 4 | fn it_works() { 5 | #[derive(DocumentedFields)] 6 | #[allow(dead_code)] 7 | struct Foo { 8 | /// 1 9 | first: i32, 10 | /// 2 11 | second: i32, 12 | } 13 | 14 | assert_eq!(Foo::FIELD_DOCS.len(), 2); 15 | assert_eq!(Foo::get_field_docs("first"), Ok("1")); 16 | assert_eq!(Foo::get_field_docs("second"), Ok("2")); 17 | assert_eq!( 18 | Foo::get_field_docs("third"), 19 | Err(Error::NoSuchField("third".into())) 20 | ); 21 | } 22 | 23 | #[test] 24 | fn enum_works() { 25 | #[derive(DocumentedFields)] 26 | #[allow(dead_code)] 27 | enum Bar { 28 | /// 1 29 | First, 30 | /// 2 31 | Second, 32 | } 33 | 34 | assert_eq!(Bar::FIELD_DOCS.len(), 2); 35 | assert_eq!(Bar::get_field_docs("First"), Ok("1")); 36 | assert_eq!(Bar::get_field_docs("Second"), Ok("2")); 37 | assert_eq!( 38 | Bar::get_field_docs("Third"), 39 | Err(Error::NoSuchField("Third".into())) 40 | ); 41 | } 42 | 43 | #[test] 44 | fn union_works() { 45 | #[derive(DocumentedFields)] 46 | #[allow(dead_code)] 47 | union FooBar { 48 | /// 1 49 | first: i32, 50 | /// 2 51 | second: i32, 52 | } 53 | 54 | assert_eq!(FooBar::FIELD_DOCS.len(), 2); 55 | assert_eq!(FooBar::get_field_docs("first"), Ok("1")); 56 | assert_eq!(FooBar::get_field_docs("second"), Ok("2")); 57 | } 58 | 59 | #[test] 60 | fn unnamed_fields() { 61 | #[derive(DocumentedFields)] 62 | #[allow(dead_code)] 63 | struct Foo( 64 | /// 0 65 | i32, 66 | /// 1 67 | u32, 68 | /// 2 69 | i64, 70 | ); 71 | 72 | assert_eq!(Foo::FIELD_DOCS.len(), 3); 73 | assert_eq!(Foo::FIELD_DOCS[0], "0"); 74 | assert_eq!(Foo::FIELD_DOCS[1], "1"); 75 | assert_eq!(Foo::FIELD_DOCS[2], "2"); 76 | } 77 | 78 | #[test] 79 | fn generic_type_works() { 80 | #[derive(DocumentedFields)] 81 | #[allow(dead_code)] 82 | struct Foo { 83 | /// foo 84 | foo: T, 85 | } 86 | 87 | assert_eq!(Foo::::get_field_docs("foo"), Ok("foo")); 88 | } 89 | 90 | #[test] 91 | fn generic_type_with_bounds_works() { 92 | #[derive(DocumentedFields)] 93 | #[allow(dead_code)] 94 | struct Foo { 95 | /// foo 96 | foo: T, 97 | } 98 | 99 | assert_eq!(Foo::::get_field_docs("foo"), Ok("foo")); 100 | } 101 | 102 | #[test] 103 | fn const_generic_type_works() { 104 | #[derive(DocumentedFields)] 105 | #[allow(dead_code)] 106 | struct Foo { 107 | /// foo 108 | foo: [u8; LEN], 109 | } 110 | 111 | assert_eq!(Foo::<69>::get_field_docs("foo"), Ok("foo")); 112 | } 113 | 114 | #[test] 115 | fn lifetimed_type_works() { 116 | #[derive(DocumentedFields)] 117 | #[allow(dead_code)] 118 | struct Foo<'a> { 119 | /// foo 120 | foo: &'a u8, 121 | } 122 | 123 | assert_eq!(Foo::FIELD_NAMES, &["foo"]); 124 | assert_eq!(Foo::get_field_docs("foo"), Ok("foo")); 125 | } 126 | 127 | #[cfg(feature = "customise")] 128 | mod test_customise { 129 | use documented::DocumentedFields; 130 | 131 | #[test] 132 | fn empty_customise_works() { 133 | #[derive(DocumentedFields)] 134 | #[documented_fields()] 135 | #[allow(dead_code)] 136 | struct Doge { 137 | /// Wow, much coin 138 | coin: usize, 139 | } 140 | 141 | assert_eq!(Doge::FIELD_NAMES, &["coin"]); 142 | assert_eq!(Doge::get_field_docs("coin"), Ok("Wow, much coin")); 143 | } 144 | 145 | #[test] 146 | fn multiple_attrs_works() { 147 | #[derive(DocumentedFields)] 148 | #[documented_fields()] 149 | #[documented_fields()] 150 | #[allow(dead_code)] 151 | struct Doge { 152 | /// Wow, much coin 153 | #[documented_fields()] 154 | #[documented_fields()] 155 | coin: usize, 156 | } 157 | 158 | assert_eq!(Doge::FIELD_NAMES, &["coin"]); 159 | assert_eq!(Doge::get_field_docs("coin"), Ok("Wow, much coin")); 160 | } 161 | 162 | #[test] 163 | fn container_customise_works() { 164 | #[derive(DocumentedFields)] 165 | #[documented_fields(trim = false)] 166 | #[allow(dead_code)] 167 | struct Doge { 168 | /// Wow, much coin 169 | coin: usize, 170 | /// Wow, much doge 171 | doge: bool, 172 | } 173 | 174 | assert_eq!(Doge::FIELD_NAMES, &["coin", "doge"]); 175 | assert_eq!(Doge::get_field_docs("coin"), Ok(" Wow, much coin")); 176 | assert_eq!(Doge::get_field_docs("doge"), Ok(" Wow, much doge")); 177 | } 178 | 179 | #[test] 180 | fn field_customise_works() { 181 | #[derive(DocumentedFields)] 182 | #[allow(dead_code)] 183 | struct Doge { 184 | /// Wow, much coin 185 | #[documented_fields(trim = false)] 186 | coin: usize, 187 | /// Wow, much doge 188 | doge: bool, 189 | } 190 | 191 | assert_eq!(Doge::FIELD_NAMES, &["coin", "doge"]); 192 | assert_eq!(Doge::get_field_docs("coin"), Ok(" Wow, much coin")); 193 | assert_eq!(Doge::get_field_docs("doge"), Ok("Wow, much doge")); 194 | } 195 | 196 | #[test] 197 | fn field_customise_override_works() { 198 | #[derive(DocumentedFields)] 199 | #[documented_fields(trim = false)] 200 | #[allow(dead_code)] 201 | struct Doge { 202 | /// Wow, much coin 203 | #[documented_fields(trim = true)] 204 | coin: usize, 205 | /// Wow, much doge 206 | doge: bool, 207 | } 208 | 209 | assert_eq!(Doge::FIELD_NAMES, &["coin", "doge"]); 210 | assert_eq!(Doge::get_field_docs("coin"), Ok("Wow, much coin")); 211 | assert_eq!(Doge::get_field_docs("doge"), Ok(" Wow, much doge")); 212 | } 213 | 214 | #[test] 215 | fn default_works() { 216 | #[derive(DocumentedFields)] 217 | #[documented_fields(default = "Woosh")] 218 | #[allow(dead_code)] 219 | enum Mission { 220 | /// Rumble 221 | Launch, 222 | Boost, 223 | // this is not very useful here, but for `*Opt` macros it is 224 | #[documented_fields(default = "Boom")] 225 | Touchdown, 226 | } 227 | 228 | assert_eq!(Mission::FIELD_NAMES, &["Launch", "Boost", "Touchdown"]); 229 | assert_eq!(Mission::get_field_docs("Launch"), Ok("Rumble")); 230 | assert_eq!(Mission::get_field_docs("Boost"), Ok("Woosh")); 231 | assert_eq!(Mission::get_field_docs("Touchdown"), Ok("Boom")); 232 | } 233 | 234 | #[test] 235 | fn rename_and_rename_all_work() { 236 | #[derive(DocumentedFields)] 237 | #[documented_fields(rename_all = "SCREAMING-KEBAB-CASE")] 238 | #[allow(dead_code)] 239 | struct AlwaysWinning { 240 | /// Gotta be opposite. 241 | opposite_colour_bishops: bool, 242 | /// Gotta have rooks. 243 | #[documented_fields(rename_all = "lowercase", rename = "some-ROOKS")] 244 | rooks: bool, 245 | /// Gotta have some pawns. 246 | #[documented_fields(rename_all = "kebab-case")] 247 | some_pawns: bool, 248 | } 249 | 250 | assert_eq!( 251 | AlwaysWinning::FIELD_NAMES, 252 | &["OPPOSITE-COLOUR-BISHOPS", "some-ROOKS", "some-pawns"] 253 | ); 254 | assert_eq!( 255 | AlwaysWinning::get_field_docs("OPPOSITE-COLOUR-BISHOPS"), 256 | Ok("Gotta be opposite.") 257 | ); 258 | assert_eq!( 259 | AlwaysWinning::get_field_docs("some-ROOKS"), 260 | Ok("Gotta have rooks.") 261 | ); 262 | assert_eq!( 263 | AlwaysWinning::get_field_docs("some-pawns"), 264 | Ok("Gotta have some pawns.") 265 | ); 266 | } 267 | 268 | #[test] 269 | fn can_set_name_for_unnamed_fields() { 270 | #[derive(DocumentedFields)] 271 | #[allow(dead_code)] 272 | struct OkYouWin( 273 | /// Leave me alone. 274 | #[documented_fields(rename = "ahhh")] 275 | (), 276 | /// Just kidding. 277 | usize, 278 | ); 279 | 280 | assert_eq!(OkYouWin::FIELD_NAMES, &["ahhh"]); 281 | assert_eq!(OkYouWin::FIELD_DOCS.len(), 2); 282 | assert_eq!(OkYouWin::FIELD_DOCS, ["Leave me alone.", "Just kidding."]); 283 | assert_eq!(OkYouWin::get_field_docs("ahhh"), Ok("Leave me alone.")); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /documented-test/src/derive/documented_fields_opt.rs: -------------------------------------------------------------------------------- 1 | use documented::{DocumentedFieldsOpt, Error}; 2 | 3 | #[test] 4 | fn it_works() { 5 | #[derive(DocumentedFieldsOpt)] 6 | #[allow(dead_code)] 7 | struct Foo { 8 | /// 1 9 | first: i32, 10 | second: i32, 11 | /// 3 12 | third: i32, 13 | } 14 | 15 | assert_eq!(Foo::FIELD_NAMES, vec!["first", "second", "third"]); 16 | assert_eq!(Foo::FIELD_DOCS.len(), 3); 17 | assert_eq!(Foo::get_field_docs("first"), Ok("1")); 18 | assert_eq!( 19 | Foo::get_field_docs("second"), 20 | Err(Error::NoDocComments("second".into())) 21 | ); 22 | assert_eq!(Foo::get_field_docs("third"), Ok("3")); 23 | assert_eq!( 24 | Foo::get_field_docs("fourth"), 25 | Err(Error::NoSuchField("fourth".into())) 26 | ); 27 | } 28 | 29 | #[test] 30 | fn enum_works() { 31 | #[derive(DocumentedFieldsOpt)] 32 | #[allow(dead_code)] 33 | enum Bar { 34 | First, 35 | /// 2 36 | Second, 37 | } 38 | 39 | assert_eq!(Bar::FIELD_NAMES, vec!["First", "Second"]); 40 | assert_eq!(Bar::FIELD_DOCS.len(), 2); 41 | assert_eq!( 42 | Bar::get_field_docs("First"), 43 | Err(Error::NoDocComments("First".into())) 44 | ); 45 | assert_eq!(Bar::get_field_docs("Second"), Ok("2")); 46 | assert_eq!( 47 | Bar::get_field_docs("Third"), 48 | Err(Error::NoSuchField("Third".into())) 49 | ); 50 | } 51 | 52 | #[test] 53 | fn union_works() { 54 | #[derive(DocumentedFieldsOpt)] 55 | #[allow(dead_code)] 56 | union FooBar { 57 | first: i32, 58 | /// 2 59 | second: i32, 60 | third: i32, 61 | } 62 | 63 | assert_eq!(FooBar::FIELD_NAMES, vec!["first", "second", "third"]); 64 | assert_eq!(FooBar::FIELD_DOCS.len(), 3); 65 | assert_eq!( 66 | FooBar::get_field_docs("first"), 67 | Err(Error::NoDocComments("first".into())) 68 | ); 69 | assert_eq!(FooBar::get_field_docs("second"), Ok("2")); 70 | assert_eq!( 71 | FooBar::get_field_docs("third"), 72 | Err(Error::NoDocComments("third".into())) 73 | ); 74 | } 75 | 76 | #[cfg(feature = "customise")] 77 | mod test_customise { 78 | use documented::{DocumentedFieldsOpt, Error}; 79 | 80 | #[test] 81 | fn default_works() { 82 | #[derive(DocumentedFieldsOpt)] 83 | #[documented_fields(default = Some("Woosh"))] 84 | #[allow(dead_code)] 85 | enum Mission { 86 | /// Rumble 87 | Launch, 88 | Boost, 89 | #[documented_fields(default = None)] 90 | FreeFall, 91 | #[documented_fields(default = Some("Boom"))] 92 | Touchdown, 93 | } 94 | 95 | assert_eq!( 96 | Mission::FIELD_NAMES, 97 | vec!["Launch", "Boost", "FreeFall", "Touchdown"] 98 | ); 99 | assert_eq!(Mission::get_field_docs("Launch"), Ok("Rumble")); 100 | assert_eq!(Mission::get_field_docs("Boost"), Ok("Woosh")); 101 | assert_eq!( 102 | Mission::get_field_docs("FreeFall"), 103 | Err(Error::NoDocComments("FreeFall".into())) 104 | ); 105 | assert_eq!(Mission::get_field_docs("Touchdown"), Ok("Boom")); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /documented-test/src/derive/documented_opt.rs: -------------------------------------------------------------------------------- 1 | use documented::DocumentedOpt; 2 | 3 | #[test] 4 | fn some_works() { 5 | /// 69 6 | #[derive(DocumentedOpt)] 7 | struct Nice; 8 | 9 | assert_eq!(Nice::DOCS, Some("69")); 10 | } 11 | 12 | #[test] 13 | fn none_works() { 14 | #[derive(DocumentedOpt)] 15 | struct NotSoNice; 16 | 17 | assert_eq!(NotSoNice::DOCS, None); 18 | } 19 | 20 | #[cfg(feature = "customise")] 21 | mod test_customise { 22 | use documented::DocumentedOpt; 23 | 24 | #[test] 25 | fn default_works() { 26 | #[derive(DocumentedOpt)] 27 | #[documented(default = Some("Nice catch!"))] 28 | struct NiceFlight; 29 | 30 | assert_eq!(NiceFlight::DOCS, Some("Nice catch!")); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /documented-test/src/derive/documented_variants.rs: -------------------------------------------------------------------------------- 1 | use documented::DocumentedVariants; 2 | 3 | #[test] 4 | fn it_works() { 5 | #[derive(DocumentedVariants)] 6 | enum Foo { 7 | /// 1 8 | First, 9 | /// 2 10 | Second, 11 | } 12 | 13 | assert_eq!(Foo::First.get_variant_docs(), "1"); 14 | assert_eq!(Foo::Second.get_variant_docs(), "2"); 15 | } 16 | 17 | #[test] 18 | fn works_on_adt_enums() { 19 | #[allow(dead_code)] 20 | #[derive(DocumentedVariants)] 21 | enum Bar { 22 | /// A unit variant. 23 | Unit, 24 | /// A 0-tuple variant. 25 | Tuple0(), 26 | /// A 1-tuple variant. 27 | Tuple1(u8), 28 | /// A 2-tuple variant. 29 | Tuple2(u8, u16), 30 | /// A struct variant. 31 | Struct { alpha: u8, bravo: u16 }, 32 | /// An empty struct variant. 33 | StructEmpty {}, 34 | } 35 | 36 | assert_eq!(Bar::Unit.get_variant_docs(), "A unit variant."); 37 | assert_eq!(Bar::Tuple0().get_variant_docs(), "A 0-tuple variant."); 38 | assert_eq!(Bar::Tuple1(1).get_variant_docs(), "A 1-tuple variant."); 39 | assert_eq!(Bar::Tuple2(2, 2).get_variant_docs(), "A 2-tuple variant."); 40 | assert_eq!( 41 | Bar::Struct { alpha: 0, bravo: 0 }.get_variant_docs(), 42 | "A struct variant." 43 | ); 44 | assert_eq!( 45 | Bar::StructEmpty {}.get_variant_docs(), 46 | "An empty struct variant." 47 | ); 48 | } 49 | 50 | #[test] 51 | fn works_on_generic_enums() { 52 | #[allow(dead_code)] 53 | #[derive(DocumentedVariants)] 54 | enum Foo { 55 | /// 600 56 | Rufus(T), 57 | /// 599 58 | Dufus(T, U), 59 | } 60 | 61 | assert_eq!(Foo::::Rufus(69).get_variant_docs(), "600"); 62 | assert_eq!(Foo::Dufus(69, 420).get_variant_docs(), "599"); 63 | } 64 | 65 | #[test] 66 | fn works_on_generic_enums_with_bounds() { 67 | #[allow(dead_code)] 68 | #[derive(DocumentedVariants)] 69 | enum Foo { 70 | /// 600 71 | Rufus(T), 72 | /// 599 73 | Dufus(T, U), 74 | } 75 | 76 | assert_eq!(Foo::::Rufus(69).get_variant_docs(), "600"); 77 | assert_eq!(Foo::Dufus(69, 420).get_variant_docs(), "599"); 78 | } 79 | 80 | #[test] 81 | fn works_on_const_generic_enums() { 82 | #[allow(dead_code)] 83 | #[derive(DocumentedVariants)] 84 | enum Foo { 85 | /// 600 86 | Rufus([u8; LEN]), 87 | /// 599 88 | Dufus([i8; LEN]), 89 | } 90 | 91 | assert_eq!(Foo::Rufus([42; 69]).get_variant_docs(), "600"); 92 | assert_eq!(Foo::Dufus([42; 69]).get_variant_docs(), "599"); 93 | } 94 | 95 | #[test] 96 | fn works_on_lifetimed_enums() { 97 | #[allow(dead_code)] 98 | #[derive(DocumentedVariants)] 99 | enum Foo<'a, T> { 100 | /// 600 101 | Rufus(&'a T), 102 | /// 599 103 | Dufus(T, &'a T), 104 | } 105 | 106 | assert_eq!(Foo::Rufus(&69).get_variant_docs(), "600"); 107 | assert_eq!(Foo::Dufus(69, &420).get_variant_docs(), "599"); 108 | } 109 | 110 | #[cfg(feature = "customise")] 111 | mod test_customise { 112 | use documented::DocumentedVariants; 113 | 114 | #[test] 115 | fn empty_customise_works() { 116 | #[derive(DocumentedVariants)] 117 | #[documented_variants()] 118 | #[allow(dead_code)] 119 | enum Name { 120 | /// Wow 121 | Doge, 122 | /// RIP 123 | Kabuso, 124 | } 125 | 126 | assert_eq!(Name::Doge.get_variant_docs(), "Wow"); 127 | assert_eq!(Name::Kabuso.get_variant_docs(), "RIP"); 128 | } 129 | 130 | #[test] 131 | fn multiple_attrs_works() { 132 | #[derive(DocumentedVariants)] 133 | #[documented_variants()] 134 | #[documented_variants()] 135 | #[allow(dead_code)] 136 | enum Name { 137 | /// Wow 138 | #[documented_variants()] 139 | #[documented_variants()] 140 | Doge, 141 | /// RIP 142 | Kabuso, 143 | } 144 | 145 | assert_eq!(Name::Doge.get_variant_docs(), "Wow"); 146 | assert_eq!(Name::Kabuso.get_variant_docs(), "RIP"); 147 | } 148 | 149 | #[test] 150 | fn container_customise_works() { 151 | #[derive(DocumentedVariants)] 152 | #[documented_variants(trim = false)] 153 | #[allow(dead_code)] 154 | enum Name { 155 | /// Wow 156 | Doge, 157 | /// RIP 158 | Kabuso, 159 | } 160 | 161 | assert_eq!(Name::Doge.get_variant_docs(), " Wow"); 162 | assert_eq!(Name::Kabuso.get_variant_docs(), " RIP"); 163 | } 164 | 165 | #[test] 166 | fn field_customise_works() { 167 | #[derive(DocumentedVariants)] 168 | #[allow(dead_code)] 169 | enum Name { 170 | /// Wow 171 | #[documented_variants(trim = false)] 172 | Doge, 173 | /// RIP 174 | Kabuso, 175 | } 176 | 177 | assert_eq!(Name::Doge.get_variant_docs(), " Wow"); 178 | assert_eq!(Name::Kabuso.get_variant_docs(), "RIP"); 179 | } 180 | 181 | #[test] 182 | fn field_customise_override_works() { 183 | #[derive(DocumentedVariants)] 184 | #[documented_variants(trim = false)] 185 | #[allow(dead_code)] 186 | enum Name { 187 | /// Wow 188 | #[documented_variants(trim = true)] 189 | Doge, 190 | /// RIP 191 | Kabuso, 192 | } 193 | 194 | assert_eq!(Name::Doge.get_variant_docs(), "Wow"); 195 | assert_eq!(Name::Kabuso.get_variant_docs(), " RIP"); 196 | } 197 | 198 | #[test] 199 | fn default_works() { 200 | #[derive(DocumentedVariants)] 201 | #[documented_variants(default = "RIP")] 202 | #[allow(dead_code)] 203 | enum Dead { 204 | Maggie, 205 | /// Maybe not yet? 206 | DotIO, 207 | // don't know why anyone would want to do this but it's supported 208 | #[documented_variants(default = "I think you're more prepared than Noah")] 209 | Sean, 210 | } 211 | 212 | assert_eq!(Dead::Maggie.get_variant_docs(), "RIP"); 213 | assert_eq!(Dead::DotIO.get_variant_docs(), "Maybe not yet?"); 214 | assert_eq!( 215 | Dead::Sean.get_variant_docs(), 216 | "I think you're more prepared than Noah" 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /documented-test/src/derive/documented_variants_opt.rs: -------------------------------------------------------------------------------- 1 | use documented::DocumentedVariantsOpt; 2 | 3 | #[test] 4 | fn it_works() { 5 | #[derive(DocumentedVariantsOpt)] 6 | enum Foo { 7 | First, 8 | /// 2 9 | Second, 10 | } 11 | 12 | assert_eq!(Foo::First.get_variant_docs(), None); 13 | assert_eq!(Foo::Second.get_variant_docs(), Some("2")); 14 | } 15 | 16 | #[cfg(feature = "customise")] 17 | mod test_customise { 18 | use documented::DocumentedVariantsOpt; 19 | 20 | #[test] 21 | fn default_works() { 22 | #[derive(DocumentedVariantsOpt)] 23 | #[documented_variants(default = Some("RIP"))] 24 | #[allow(dead_code)] 25 | enum Dead { 26 | Maggie, 27 | /// Maybe not? 28 | DotIO, 29 | #[documented_variants(default = Some("I think you're more prepared than Noah"))] 30 | Sean, 31 | // ah so here's a semi-reasonable use case for this 32 | #[documented_variants(default = None)] 33 | OJ, 34 | } 35 | 36 | assert_eq!(Dead::Maggie.get_variant_docs(), Some("RIP")); 37 | assert_eq!(Dead::DotIO.get_variant_docs(), Some("Maybe not?")); 38 | assert_eq!( 39 | Dead::Sean.get_variant_docs(), 40 | Some("I think you're more prepared than Noah") 41 | ); 42 | assert_eq!(Dead::OJ.get_variant_docs(), None); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /documented-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | mod attr; 4 | mod derive; 5 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1681202837, 27 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1694959747, 42 | "narHash": "sha256-CXQ2MuledDVlVM5dLC4pB41cFlBWxRw4tCBsFrq3cRk=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "970a59bd19eff3752ce552935687100c46e820a5", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "id": "nixpkgs", 50 | "ref": "nixos-unstable", 51 | "type": "indirect" 52 | } 53 | }, 54 | "nixpkgs_2": { 55 | "locked": { 56 | "lastModified": 1681358109, 57 | "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", 58 | "owner": "NixOS", 59 | "repo": "nixpkgs", 60 | "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", 61 | "type": "github" 62 | }, 63 | "original": { 64 | "owner": "NixOS", 65 | "ref": "nixpkgs-unstable", 66 | "repo": "nixpkgs", 67 | "type": "github" 68 | } 69 | }, 70 | "root": { 71 | "inputs": { 72 | "flake-utils": "flake-utils", 73 | "nixpkgs": "nixpkgs", 74 | "rust-overlay": "rust-overlay" 75 | } 76 | }, 77 | "rust-overlay": { 78 | "inputs": { 79 | "flake-utils": "flake-utils_2", 80 | "nixpkgs": "nixpkgs_2" 81 | }, 82 | "locked": { 83 | "lastModified": 1695175880, 84 | "narHash": "sha256-TBR5/K3jkrd+U5mjxvRvUhlcT1Hw9jFywz1TjAGZRm4=", 85 | "owner": "oxalica", 86 | "repo": "rust-overlay", 87 | "rev": "e054ca37ee416efe9d8fc72d249ec332ef74b6d4", 88 | "type": "github" 89 | }, 90 | "original": { 91 | "owner": "oxalica", 92 | "repo": "rust-overlay", 93 | "type": "github" 94 | } 95 | }, 96 | "systems": { 97 | "locked": { 98 | "lastModified": 1681028828, 99 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 100 | "owner": "nix-systems", 101 | "repo": "default", 102 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 103 | "type": "github" 104 | }, 105 | "original": { 106 | "owner": "nix-systems", 107 | "repo": "default", 108 | "type": "github" 109 | } 110 | }, 111 | "systems_2": { 112 | "locked": { 113 | "lastModified": 1681028828, 114 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 115 | "owner": "nix-systems", 116 | "repo": "default", 117 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 118 | "type": "github" 119 | }, 120 | "original": { 121 | "owner": "nix-systems", 122 | "repo": "default", 123 | "type": "github" 124 | } 125 | } 126 | }, 127 | "root": "root", 128 | "version": 7 129 | } 130 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Trait and derive macro for accessing your type's documentation at runtime"; 3 | 4 | inputs.nixpkgs.url = "nixpkgs/nixos-unstable"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | inputs.rust-overlay.url = "github:oxalica/rust-overlay"; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | flake-utils, 13 | rust-overlay, 14 | }: 15 | flake-utils.lib.eachDefaultSystem 16 | ( 17 | system: let 18 | overlays = [(import rust-overlay)]; 19 | pkgs = import nixpkgs { 20 | inherit system overlays; 21 | }; 22 | rustTarget = 23 | pkgs.rust-bin.stable.latest.default.override 24 | { 25 | extensions = ["rust-analyzer" "rust-src"]; 26 | }; 27 | nativeBuildInputs = with pkgs; [ 28 | curl 29 | gcc 30 | pkg-config 31 | which 32 | zlib 33 | ]; 34 | buildInputs = with pkgs; [ 35 | ]; 36 | in { 37 | devShells.default = pkgs.mkShell { 38 | nativeBuildInputs = 39 | nativeBuildInputs 40 | ++ [ 41 | ]; 42 | buildInputs = 43 | buildInputs 44 | ++ [ 45 | rustTarget 46 | ]; 47 | }; 48 | } 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors.workspace = true 3 | categories.workspace = true 4 | description = "Derive and attribute macros for accessing your type's documentation at runtime" 5 | edition.workspace = true 6 | keywords.workspace = true 7 | license.workspace = true 8 | name = "documented" 9 | readme.workspace = true 10 | repository.workspace = true 11 | rust-version.workspace = true 12 | version.workspace = true 13 | 14 | [dependencies] 15 | documented-macros = { path = "../documented-macros", version = "=0.9.1" } 16 | phf = { version = "0.11", default-features = false, features = ["macros"] } 17 | thiserror = "2.0.11" 18 | 19 | [features] 20 | customise = ["documented-macros/customise"] 21 | default = ["customise"] 22 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))] 2 | 3 | pub use documented_macros::{ 4 | docs_const, Documented, DocumentedFields, DocumentedFieldsOpt, DocumentedOpt, 5 | DocumentedVariants, DocumentedVariantsOpt, 6 | }; 7 | 8 | #[doc(hidden)] 9 | pub use phf as _private_phf_reexport_for_macro; 10 | 11 | /// Adds an associated constant [`DOCS`](Self::DOCS) on your type containing its 12 | /// documentation, allowing you to access its documentation at runtime. 13 | /// 14 | /// The associated derive macro of this trait will error if the type does not 15 | /// have any doc comments. Use [`DocumentedOpt`] if this is undesirable. 16 | /// 17 | /// For how to use the derive macro, see [`macro@Documented`]. 18 | pub trait Documented { 19 | /// The static doc comments on this type. 20 | const DOCS: &'static str; 21 | } 22 | 23 | /// The optional variant of [`Documented`]. 24 | pub trait DocumentedOpt { 25 | /// The static doc comments on this type. 26 | const DOCS: Option<&'static str>; 27 | } 28 | 29 | /// Adds an associated constant [`FIELD_DOCS`](Self::FIELD_DOCS) on your type 30 | /// containing the documentation of its fields, allowing you to access their 31 | /// documentation at runtime. 32 | /// 33 | /// The associated derive macro of this trait will error if any field or variant 34 | /// does not have any doc comments. 35 | /// Use [`DocumentedFieldsOpt`] if this is undesirable. 36 | /// 37 | /// This trait and associated derive macro works on structs, enums, and unions. 38 | /// For enums, you may find [`DocumentedVariants`] more ergonomic to use. 39 | /// 40 | /// For how to use the derive macro, see [`macro@DocumentedFields`]. 41 | pub trait DocumentedFields { 42 | /// The static doc comments on each field or variant of this type, indexed 43 | /// by field/variant order. 44 | const FIELD_DOCS: &'static [&'static str]; 45 | /// Field names, as accepted by [`Self::get_field_docs`]. 46 | /// 47 | /// Note that anonymous fields (i.e. fields in tuple structs), unless they 48 | /// have [a custom name set](macro@DocumentedFields#2-set-a-custom-name-for-a-specific-field-for-get_field_docs-like-so), 49 | /// will be omitted from `FIELD_NAMES`. This means that in such cases, the 50 | /// indices of `FIELD_NAMES` will be misaligned with that of `FIELD_DOCS`. 51 | /// 52 | /// It is therefore recommended to use [`Self::get_field_docs`] rather than 53 | /// the index to lookup the corresponding documentation. 54 | const FIELD_NAMES: &'static [&'static str]; 55 | 56 | /// Method internally used by `documented`. 57 | #[doc(hidden)] 58 | fn __documented_get_index>(field_name: T) -> Option; 59 | 60 | /// Get a field's documentation using its name. 61 | /// 62 | /// Note that for structs with anonymous fields (i.e. tuple structs), this 63 | /// method will always return [`Error::NoSuchField`] by default. For this 64 | /// case, you can either: 65 | /// 66 | /// 1. use [`FIELD_DOCS`](Self::FIELD_DOCS) directly instead; 67 | /// 2. [set a custom name](macro@DocumentedFields#2-set-a-custom-name-for-a-specific-field-for-get_field_docs-like-so) for the anonymous field. 68 | fn get_field_docs>(field_name: T) -> Result<&'static str, Error> { 69 | let field_name = field_name.as_ref(); 70 | let index = Self::__documented_get_index(field_name) 71 | .ok_or_else(|| Error::NoSuchField(field_name.into()))?; 72 | Ok(Self::FIELD_DOCS[index]) 73 | } 74 | } 75 | 76 | /// The optional variant of [`DocumentedFields`]. 77 | pub trait DocumentedFieldsOpt { 78 | /// The static doc comments on each field or variant of this type, indexed 79 | /// by field/variant order. 80 | const FIELD_DOCS: &'static [Option<&'static str>]; 81 | /// Field names, as accepted by [`Self::get_field_docs`]. 82 | /// 83 | /// Note that anonymous fields (i.e. fields in tuple structs), unless they 84 | /// have [a custom name set](macro@DocumentedFields#2-set-a-custom-name-for-a-specific-field-for-get_field_docs-like-so), 85 | /// will be omitted from `FIELD_NAMES`. This means that in such cases, the 86 | /// indices of `FIELD_NAMES` will be misaligned with that of `FIELD_DOCS`. 87 | /// 88 | /// It is therefore recommended to use [`Self::get_field_docs`] rather than 89 | /// the index to lookup the corresponding documentation. 90 | const FIELD_NAMES: &'static [&'static str]; 91 | 92 | /// Method internally used by `documented`. 93 | #[doc(hidden)] 94 | fn __documented_get_index>(field_name: T) -> Option; 95 | 96 | /// Get a field's documentation using its name. 97 | /// 98 | /// Note that for structs with anonymous fields (i.e. tuple structs), this 99 | /// method will always return [`Error::NoSuchField`] by default. For this 100 | /// case, you can either: 101 | /// 102 | /// 1. use [`FIELD_DOCS`](Self::FIELD_DOCS) directly instead; 103 | /// 2. [set a custom name](macro@DocumentedFields#2-set-a-custom-name-for-a-specific-field-for-get_field_docs-like-so) for the anonymous field. 104 | fn get_field_docs>(field_name: T) -> Result<&'static str, Error> { 105 | let field_name = field_name.as_ref(); 106 | let index = Self::__documented_get_index(field_name) 107 | .ok_or_else(|| Error::NoSuchField(field_name.into()))?; 108 | Self::FIELD_DOCS[index].ok_or_else(|| Error::NoDocComments(field_name.into())) 109 | } 110 | } 111 | 112 | /// Adds an associated function [`get_variant_docs`](Self::get_variant_docs) to 113 | /// access the documentation on an enum variant. 114 | /// 115 | /// The associated derive macro of this trait will error if any variant does not 116 | /// have any doc comments. Use [`DocumentedVariantsOpt`] if this is undesirable. 117 | /// 118 | /// This trait and associated derive macro works on enums only. For structs and 119 | /// unions, use [`DocumentedFields`] instead. 120 | /// 121 | /// For how to use the derive macro, see [`macro@DocumentedVariants`]. 122 | pub trait DocumentedVariants { 123 | /// Get the documentation on this enum variant. 124 | fn get_variant_docs(&self) -> &'static str; 125 | } 126 | 127 | /// The optional variant of [`DocumentedVariants`]. 128 | pub trait DocumentedVariantsOpt { 129 | /// Get the documentation on this enum variant. 130 | fn get_variant_docs(&self) -> Option<&'static str>; 131 | } 132 | 133 | /// Errors of `documented`. 134 | #[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] 135 | pub enum Error { 136 | /// The requested field does not have doc comments. 137 | #[error(r#"The field "{0}" has no doc comments"#)] 138 | NoDocComments(String), 139 | /// The requested field does not exist. 140 | #[error(r#"No field named "{0}" exists"#)] 141 | NoSuchField(String), 142 | } 143 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 # See https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#width_heuristics 2 | single_line_if_else_max_width = 60 3 | struct_lit_width = 40 4 | struct_variant_width = 50 5 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # Just uses the flake. For the nix-env addon (which is kind of dead) users, I use the direnv addon. 2 | (builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default 3 | --------------------------------------------------------------------------------