├── .gitignore ├── Cargo.toml ├── LICENSE ├── fondant ├── Cargo.toml ├── LICENSE ├── readme.md └── src │ └── lib.rs ├── fondant_deps ├── Cargo.toml ├── LICENSE ├── readme.md └── src │ └── lib.rs ├── fondant_derive ├── Cargo.toml ├── LICENSE ├── readme.md └── src │ └── lib.rs └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/** 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "fondant", 5 | "fondant_derive", 6 | "fondant_deps", 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Akshay Oppiliappan (nerdypepper@tuta.io) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /fondant/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fondant" 3 | version = "0.1.2" 4 | authors = ["Akshay "] 5 | description = "Macro based library to take the boilerplate out of configuration handling" 6 | readme = "readme.md" 7 | repository = "https://github.com/nerdypepper/fondant" 8 | license = "MIT" 9 | keywords = ["cli", "configuration", "macro"] 10 | categories = ["command-line-utilities", "config"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | fondant_derive = {version = "0.1.1", path = "../fondant_derive"} 15 | fondant_deps = {version = "0.1.1", path = "../fondant_deps"} 16 | -------------------------------------------------------------------------------- /fondant/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /fondant/readme.md: -------------------------------------------------------------------------------- 1 | /home/np/code/rustuff/fondant_workspace/readme.md -------------------------------------------------------------------------------- /fondant/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # fondant 2 | //! 3 | //! [Documentation](https://docs.rs/fondant/) · 4 | //! [Architecture](#Architecture) · [Usage](#Usage) · 5 | //! [Customization](#Customization) · [Todo](#Todo) 6 | //! 7 | //! `fondant` is a macro based library to take the boilerplate out of 8 | //! configuration handling. All you need to do is derive the 9 | //! `Configure` trait on your struct, and `fondant` will decide 10 | //! where to store it and and how to do so safely. 11 | //! 12 | //! Most of `fondant` is based off the `confy` crate. 13 | //! `fondant` adds a couple of extra features: 14 | //! 15 | //! - support for json, yaml and toml 16 | //! - support for custom config paths 17 | //! - support for custom config file names 18 | //! 19 | //! ### Architecture 20 | //! 21 | //! `fondant` is split into 3 separate crates: 22 | //! 23 | //! - `fondant_deps`: external crates and utils that `fondant` requires 24 | //! - `fondant_derive`: core macro definitions 25 | //! - `fondant`: the user facing library that brings it all together 26 | //! 27 | //! This slightly strange architecture arose because of some 28 | //! limitations with proc-macro crates and strict cyclic 29 | //! dependencies in cargo. All you need is the `fondant` crate. 30 | //! 31 | //! ### Usage 32 | //! 33 | //! ```rust 34 | //! // the struct has to derive Serialize, Deserialize and Default 35 | //! #[derive(Configure, Serialize, Deserialize, Default)] 36 | //! #[config_file = "config.toml"] 37 | //! // `config_file` attribute sets the file name to "config.toml" 38 | //! // the file format to "toml" 39 | //! // and the file path to "default" (read the notes below) 40 | //! struct AppConfig { 41 | //! version: u32, 42 | //! port: u32, 43 | //! username: String, 44 | //! } 45 | //! 46 | //! fn main() { 47 | //! // use `load` to load the config file 48 | //! // loads in Default::default if it can't find one 49 | //! let mut conf = AppConfig::load().unwrap(); 50 | //! 51 | //! // do stuff with conf 52 | //! conf.version = 2; 53 | //! 54 | //! // call `store` to save changes 55 | //! conf.store().unwrap(); 56 | //! } 57 | //! ``` 58 | //! 59 | //! **Notes**: 60 | //! - `load` returns `Default::default` if the config file is not present, and stores 61 | //! a serialized `Default::default` at the specified path 62 | //! - the "default" config path varies by platform: 63 | //! * GNU/Linux: `$XDG_CONFIG_HOME/my_cool_crate/config.toml` (follows xdg spec) 64 | //! * MacOS: `$HOME/Library/Preferences/my_cool_crate/config.toml` 65 | //! * Windows: `{FOLDERID_RoamingAppData}\_project_path_\config` 66 | //! 67 | //! ### Customization 68 | //! 69 | //! Set your own filename, for ex.: `apprc` 70 | //! 71 | //! ```rust 72 | //! #[derive(Configure, Serialize, Deserialize, Default)] 73 | //! #[config_file = "apprc.toml"] 74 | //! struct AppConfig { 75 | //! // -- snip -- 76 | //! } 77 | //! // effective path: $XDG_CONFIG_HOME/my_cool_crate/apprc.toml 78 | //! // effective format: toml 79 | //! ``` 80 | //! 81 | //! Change file format to `yaml`, by changing the file extension. 82 | //! Supported extensions are `yaml`, `toml`, `json`: 83 | //! 84 | //! ```rust 85 | //! #[derive(Configure, Serialize, Deserialize, Default)] 86 | //! #[config_file = "config.yaml"] 87 | //! struct AppConfig { 88 | //! // -- snip -- 89 | //! } 90 | //! // effective path: $XDG_CONFIG_HOME/my_cool_crate/config.yaml 91 | //! // effective format: yaml 92 | //! ``` 93 | //! 94 | //! Override the default config path (not recommended), 95 | //! for ex.: the home directory: 96 | //! 97 | //! ```rust 98 | //! #[derive(Configure, Serialize, Deserialize, Default)] 99 | //! #[config_file = "~/.apprc.json"] 100 | //! struct AppConfig { 101 | //! // -- snip -- 102 | //! } 103 | //! // effective path: $HOME/.apprc.json 104 | //! // effective format: json 105 | //! ``` 106 | //! 107 | //! Fondant meshes well with Serde, for ex.: 108 | //! ```rust 109 | //! #[derive(Configure, Serialize, Deserialize, Default)] 110 | //! #[config_file = "config.toml"] 111 | //! struct Madagascar { 112 | //! #[serde(skip)] 113 | //! rating: u32, 114 | //! 115 | //! name: String, 116 | //! penguins: u32, 117 | //! } 118 | //! ``` 119 | //! 120 | //! Above snippet produces this config file: 121 | //! ```toml 122 | //! name = 'Central Park Zoo' 123 | //! penguins = 4 124 | //! ``` 125 | 126 | pub use fondant_deps::fondant_exports; 127 | pub use fondant_deps::Configure; 128 | pub use fondant_deps::FondantError; 129 | pub use fondant_derive::Configure; 130 | -------------------------------------------------------------------------------- /fondant_deps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fondant_deps" 3 | version = "0.1.1" 4 | authors = ["Akshay "] 5 | description = "External dependencies to supplement fondant" 6 | readme = "readme.md" 7 | repository = "https://github.com/nerdypepper/fondant" 8 | license = "MIT" 9 | keywords = ["cli", "configuration", "macro"] 10 | categories = ["command-line-utilities", "config"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | toml = "^0.5" 15 | serde_yaml = "0.8" 16 | serde_json = "1.0.48" 17 | directories = "2.0" 18 | 19 | [dependencies.serde] 20 | version = "1.0.103" 21 | features = ["derive"] 22 | 23 | [dependencies.syn] 24 | version = "1.0" 25 | features = ["full"] 26 | -------------------------------------------------------------------------------- /fondant_deps/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /fondant_deps/readme.md: -------------------------------------------------------------------------------- 1 | ### fondant_deps 2 | 3 | > external crates and utils that `fondant` requires 4 | -------------------------------------------------------------------------------- /fondant_deps/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod fondant_exports { 2 | pub use directories::{ProjectDirs, UserDirs}; 3 | pub use serde::{de::DeserializeOwned, Serialize}; 4 | pub use serde_json; 5 | pub use serde_yaml; 6 | use std::path::{Path, PathBuf}; 7 | pub use toml; 8 | pub fn expand_tilde>(path: P) -> PathBuf { 9 | let p = path.as_ref(); 10 | if p.starts_with("~") { 11 | if p == Path::new("~") { 12 | return UserDirs::new().unwrap().home_dir().to_path_buf(); 13 | } else { 14 | let mut h = UserDirs::new().unwrap().home_dir().to_path_buf(); 15 | h.push(p.strip_prefix("~/").unwrap()); 16 | return h; 17 | } 18 | } 19 | return p.to_path_buf(); 20 | } 21 | } 22 | 23 | use serde::{de::DeserializeOwned, Serialize}; 24 | use std::error::Error; 25 | use std::fmt; 26 | use std::path::Path; 27 | 28 | #[derive(Debug)] 29 | /// Errors that `load` and `store` can result in 30 | pub enum FondantError { 31 | /// Occurs when the home dir is not accessible. 32 | /// You should probably `panic!` when this is thrown. 33 | InvalidHomeDir, 34 | 35 | /// Invalid toml/yaml/json config. 36 | ConfigParseError, 37 | 38 | /// Invalid permissions to create config dir. 39 | /// Might occur when you set config dir to, say, `/etc/config.toml` and run without superuser. 40 | DirCreateErr(std::io::Error), 41 | LoadError, 42 | FileWriteError, 43 | FileReadError, 44 | FileOpenError, 45 | } 46 | 47 | impl fmt::Display for FondantError { 48 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | let suggestion_text = 50 | "HELP: You might have insufficient permissions to perform this action."; 51 | match self { 52 | FondantError::InvalidHomeDir => write!(f, "Failed to find home directory!"), 53 | FondantError::ConfigParseError => write!(f, "Invalid configuration file!"), 54 | FondantError::DirCreateErr(_) => { 55 | write!(f, "Failed to write to configuration directory!") 56 | } 57 | FondantError::LoadError => write!(f, "Failed to load configuration file!"), 58 | FondantError::FileWriteError => { 59 | write!(f, "Failed to write configuration file! {}", suggestion_text) 60 | } 61 | FondantError::FileReadError => { 62 | write!(f, "Failed to read configuration file! {}", suggestion_text) 63 | } 64 | FondantError::FileOpenError => { 65 | write!(f, "Failed to open configuration file! {}", suggestion_text) 66 | } 67 | } 68 | } 69 | } 70 | 71 | impl Error for FondantError {} 72 | 73 | /// Derive this trait on a struct to mark it as a 'configuration' struct. 74 | pub trait Configure: Serialize + DeserializeOwned + Default { 75 | fn load_file>(config_file: P) -> Result; 76 | fn load() -> Result; 77 | fn store(&self) -> Result<(), FondantError>; 78 | fn store_file>(&self, config_file: P) -> Result<(), FondantError>; 79 | } 80 | -------------------------------------------------------------------------------- /fondant_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fondant_derive" 3 | version = "0.1.1" 4 | authors = ["Akshay "] 5 | description = "Core macros of fondant" 6 | readme = "readme.md" 7 | repository = "https://github.com/nerdypepper/fondant" 8 | license = "MIT" 9 | keywords = ["cli", "configuration", "macro"] 10 | categories = ["command-line-utilities", "config"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0.9" 15 | quote = "1.0" 16 | 17 | [dependencies.syn] 18 | version = "1.0" 19 | features = ["full"] 20 | 21 | [lib] 22 | proc-macro = true 23 | -------------------------------------------------------------------------------- /fondant_derive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /fondant_derive/readme.md: -------------------------------------------------------------------------------- 1 | ### fondant_derive 2 | 3 | > core macro definitions 4 | -------------------------------------------------------------------------------- /fondant_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Please refer to the `fondant` crate for usage instructions 2 | 3 | extern crate proc_macro; 4 | 5 | use ::std::ffi::{OsStr, OsString}; 6 | use ::std::path::Path; 7 | use proc_macro::TokenStream; 8 | use proc_macro2::Span; 9 | use quote::quote; 10 | use syn::{parse_macro_input, DeriveInput, Ident, Lit, Meta, MetaNameValue}; 11 | 12 | #[derive(Debug, Default)] 13 | struct ConfigPath { 14 | parent: String, 15 | filename: Option, 16 | extension: Option, 17 | } 18 | 19 | #[proc_macro_derive(Configure, attributes(config_file))] 20 | pub fn config_attribute(item: TokenStream) -> TokenStream { 21 | let ast: DeriveInput = parse_macro_input!(item as DeriveInput); 22 | let cfg_path = extract_attributes(&ast); 23 | 24 | gen_impl(&ast, cfg_path) 25 | } 26 | 27 | fn extract_attributes(ast: &DeriveInput) -> ConfigPath { 28 | for option in ast.attrs.iter() { 29 | let option = option.parse_meta().unwrap(); 30 | match option { 31 | Meta::NameValue(MetaNameValue { 32 | ref path, ref lit, .. 33 | }) if path.is_ident("config_file") => { 34 | if let Lit::Str(f) = lit { 35 | let f = f.value(); 36 | let fp = Path::new(&f); 37 | let parent = fp.parent().unwrap_or(Path::new("")); 38 | return ConfigPath { 39 | parent: parent.to_str().unwrap().into(), 40 | filename: fp.file_stem().map(OsStr::to_os_string), 41 | extension: fp.extension().map(OsStr::to_os_string), 42 | }; 43 | } 44 | } 45 | _ => {} 46 | } 47 | } 48 | return Default::default(); 49 | } 50 | 51 | fn pick_serializer(ext: &str) -> (Ident, Ident) { 52 | /* returns serializer and a corresponding function to 53 | * stringify with based on file extension 54 | * toml::to_string_pretty 55 | * serde_yaml::to_string 56 | * serde_json::to_string_pretty 57 | */ 58 | match ext.as_ref() { 59 | "toml" => ( 60 | Ident::new("toml", Span::call_site()), 61 | Ident::new("to_string_pretty", Span::call_site()), 62 | ), 63 | "yaml" => ( 64 | Ident::new("serde_yaml", Span::call_site()), 65 | Ident::new("to_string", Span::call_site()), 66 | ), 67 | "json" => ( 68 | Ident::new("serde_json", Span::call_site()), 69 | Ident::new("to_string_pretty", Span::call_site()), 70 | ), 71 | _ => panic!("Invalid extension!"), 72 | } 73 | } 74 | 75 | fn gen_impl(ast: &DeriveInput, cfg_path: ConfigPath) -> TokenStream { 76 | let struct_ident = &ast.ident; 77 | 78 | let filename = cfg_path 79 | .filename 80 | .unwrap_or(OsStr::new("config").to_os_string()) 81 | .into_string() 82 | .unwrap(); 83 | 84 | let filetype = cfg_path 85 | .extension 86 | .unwrap_or(OsStr::new("toml").to_os_string()) 87 | .into_string() 88 | .unwrap(); 89 | 90 | let parent = cfg_path.parent; 91 | 92 | let (ser, ser_fn) = pick_serializer(&filetype); 93 | 94 | let includes = quote! { 95 | use ::fondant::fondant_exports::*; 96 | use ::fondant::FondantError ; 97 | use ::std::option::Option; 98 | use ::std::fs::{self, File, OpenOptions}; 99 | use ::std::io::prelude::*; 100 | use ::std::io::{ ErrorKind::NotFound, Write }; 101 | use ::std::ffi::{OsStr, OsString}; 102 | use ::std::path::{Path, PathBuf}; 103 | }; 104 | 105 | let load_paths = quote! { 106 | let pkg_name = env!("CARGO_PKG_NAME"); 107 | let project = ProjectDirs::from("rs", "", pkg_name).unwrap(); 108 | let default_dir: String = project.config_dir().to_str().unwrap().into(); 109 | 110 | let d = if #parent != "" { #parent.into() } else { default_dir }; 111 | let config_dir: String = expand_tilde(d) 112 | .as_path() 113 | .to_str() 114 | .unwrap() 115 | .into(); 116 | 117 | let tip = Path::new(&#filename).with_extension(&#filetype); 118 | let mut config_file = PathBuf::from(&config_dir); 119 | config_file.push(tip); 120 | }; 121 | 122 | let gen = quote! { 123 | #includes 124 | impl Configure for #struct_ident { 125 | fn load() -> Result<#struct_ident, FondantError> { 126 | #load_paths 127 | Self::load_file(&config_file) 128 | } 129 | 130 | fn load_file>(conf_file: P) -> Result<#struct_ident, FondantError> { 131 | #load_paths 132 | match File::open(&conf_file) { // Note: conf_file is different than config_file from #load_paths 133 | Ok(mut cfg) => { 134 | let mut cfg_data = String::new(); 135 | cfg.read_to_string(&mut cfg_data).unwrap(); 136 | 137 | let config: #struct_ident = #ser::from_str(&cfg_data[..]) 138 | .map_err(|_| FondantError::ConfigParseError)?; 139 | return Ok(config); 140 | }, 141 | Err(ref e) if e.kind() == NotFound => { 142 | if !Path::new(&config_dir).is_dir() { 143 | fs::create_dir_all(config_dir).map_err(FondantError::DirCreateErr)?; 144 | } 145 | let default_impl = #struct_ident::default(); 146 | Configure::store(&default_impl)?; 147 | return Ok(default_impl); 148 | }, 149 | Err(e) => return Err(FondantError::LoadError), 150 | }; 151 | } 152 | 153 | fn store(&self) -> Result<(), FondantError> { 154 | #load_paths 155 | &self.store_file(&config_file)?; 156 | Ok(()) 157 | } 158 | 159 | fn store_file>(&self, conf_file: P) -> Result<(), FondantError> { 160 | #load_paths 161 | let mut f = OpenOptions::new() 162 | .write(true) 163 | .create(true) 164 | .truncate(true) 165 | .open(conf_file) // Note: conf_file is different than config_file from #load_paths 166 | .map_err(|_| FondantError::FileOpenError)?; 167 | 168 | let s = #ser::#ser_fn(self).map_err(|_| FondantError::ConfigParseError)?; 169 | f.write_all(s.as_bytes()).map_err(|_| FondantError::FileWriteError)?; 170 | Ok(()) 171 | } 172 | } 173 | }; 174 | gen.into() 175 | } 176 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fondant 2 | 3 | [Documentation](https://docs.rs/fondant/) · 4 | [Architecture](#Architecture) · [Usage](#Usage) · 5 | [Customization](#Customization) · [Todo](#Todo) 6 | 7 | `fondant` is a macro based library to take the boilerplate out of 8 | configuration handling. All you need to do is derive the 9 | `Configure` trait on your struct, and `fondant` will decide 10 | where to store it and and how to do so safely. 11 | 12 | 13 | Most of `fondant` is based off the `confy` crate, 14 | with a couple of extra features: 15 | 16 | - support for json, yaml and toml 17 | - support for custom config paths 18 | - support for custom config file names 19 | 20 | ### Usage ([Full Documentation](https://docs.rs/fondant/)) 21 | 22 | Drop this in your `Cargo.toml` to get started: 23 | 24 | ``` 25 | [dependencies] 26 | fondant = "0.1.0" 27 | ``` 28 | 29 | Derive the macro: 30 | 31 | ```rust 32 | // the struct has to derive Serialize, Deserialize and Default 33 | use fondant::Configure; 34 | use serde::{Serialize, Deserialize}; 35 | 36 | #[derive(Configure, Serialize, Deserialize, Default)] 37 | #[config_file = "config.toml"] 38 | struct AppConfig { 39 | port: u32, 40 | username: String, 41 | } 42 | 43 | fn main() { 44 | // use `load` to load the config file 45 | // loads in Default::default if it can't find one 46 | let mut conf = AppConfig::load().unwrap(); 47 | 48 | // do stuff with conf 49 | conf.port = 7878; 50 | 51 | // call `store` to save changes 52 | conf.store().unwrap(); 53 | } 54 | ``` 55 | 56 | Find more examples and options at [docs.rs](https://docs.rs/fondant/). 57 | 58 | ### Architecture 59 | 60 | `fondant` is split into 3 separate crates: 61 | 62 | - `fondant_deps`: external crates and utils that `fondant` requires 63 | - `fondant_derive`: core macro definitions 64 | - `fondant`: the user facing library that brings it all together 65 | 66 | This slightly strange architecture arose because of some 67 | limitations with proc-macro crates and strict cyclic 68 | dependencies in cargo. All you need is the `fondant` crate. 69 | 70 | 71 | ### Todo 72 | 73 | - [x] improve error types 74 | - [ ] use `syn::Error` and `syn::Result` to report macro errors 75 | - [x] write docs 76 | - [ ] write test suite 77 | - [x] bundle and publish to crates.io 78 | 79 | --------------------------------------------------------------------------------