This file is a merged representation of the entire codebase, combined into a single document by Repomix.
<file_summary>
This section contains a summary of this file.
<purpose>
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
</purpose>
<file_format>
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
- File path as an attribute
- Full contents of the file
</file_format>
<usage_guidelines>
- This file should be treated as read-only. Any changes should be made to the
original repository files, not this packed version.
- When processing this file, use the file path to distinguish
between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
the same level of security as you would the original repository.
</usage_guidelines>
<notes>
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Files are sorted by Git change count (files with more changes are at the bottom)
</notes>
</file_summary>
<directory_structure>
.claude/
settings.local.json
.github/
workflows/
rust.yml
core/
src/
cache.rs
lib.rs
Cargo.toml
macros/
src/
cache_it.rs
from_hashmap.rs
lib.rs
Cargo.toml
src/
lib.rs
tests/
cache_it_test.rs
from_hashmap_test.rs
.gitignore
Cargo.toml
README.md
repomix-output.txt
</directory_structure>
<files>
This section contains the contents of the repository's files.
<file path=".claude/settings.local.json">
{
"permissions": {
"allow": [
"Bash(cargo test:*)"
],
"deny": []
}
}
</file>
<file path="core/src/cache.rs">
use serde::{de::DeserializeOwned, Serialize};
pub trait Cache {
fn get<T: DeserializeOwned + Clone>(&self, key: &str) -> Option<T>;
fn set<T: Serialize>(&self, key: &str, value: &T);
fn set_with<T: Serialize>(&self, key: &str, value: &T, extra: u32) {
let _ = extra; // ignored by default
self.set(key, value);
}
}
</file>
<file path="core/src/lib.rs">
pub mod cache;
</file>
<file path="core/Cargo.toml">
[package]
name = "doless_core"
version = "0.1.0"
edition = "2021"
description = "Core runtime utilities for doless (e.g. DoLessCache, traits, helpers)"
license = "MIT OR Apache-2.0"
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
</file>
<file path="macros/src/cache_it.rs">
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Expr, Ident, ItemFn, Result, Token,
};
/// Structure representing parsed attribute arguments.
///
/// Example forms supported:
/// ```ignore
/// #[cache(key = format!("user:{}", id))]
/// #[cache(var = redis, key = format!("user:{}", id), name = cached)]
/// ```
struct CacheArgs {
/// The cache variable name (defaults to `cache`)
var: Option<Expr>,
/// The expression used as the cache key
key: Expr,
/// The name for the local binding of cached data (defaults to `cache_data`)
name: Option<Expr>,
}
impl Parse for CacheArgs {
fn parse(input: ParseStream) -> Result<Self> {
let mut var = None;
let mut key = None;
let mut name = None;
while !input.is_empty() {
// Parse `ident = <expr>`
let ident: Ident = input.parse()?;
input.parse::<Token![=]>()?;
let expr: Expr = input.parse()?;
if ident == "var" {
var = Some(expr);
} else if ident == "key" {
key = Some(expr);
} else if ident == "name" {
name = Some(expr);
} else {
// use clone to create new Ident for error span
return Err(syn::Error::new_spanned(
ident.clone(),
format!("unexpected argument `{}`", ident),
));
}
// optional trailing comma
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>();
}
}
Ok(CacheArgs {
var,
key: key.ok_or_else(|| input.error("missing `key = ...` argument"))?,
name,
})
}
}
/// The main `#[cache(...)]` procedural macro entry point.
///
/// Example:
/// ```ignore
/// #[cache(key = format!("user:{}", id))]
/// fn get_user(cache: &impl Cache<User>) -> Option<User> { ... }
/// ```
pub(crate) fn cache(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as CacheArgs);
let input_fn = parse_macro_input!(item as ItemFn);
match expand_cache_it(args, input_fn) {
Ok(expanded) => expanded.into(),
Err(e) => e.to_compile_error().into(),
}
}
/// Expands the cache macro into the final function implementation.
fn expand_cache_it(args: CacheArgs, input_fn: ItemFn) -> Result<TokenStream2> {
let vis = &input_fn.vis;
let sig = &input_fn.sig;
let block = &input_fn.block;
let cache_var = args.var.map_or_else(|| quote!(cache), |v| quote!(#v));
let binding_name = args.name.map_or_else(|| quote!(cache_data), |n| quote!(#n));
let key_expr = args.key;
// let maybe_ret_ty = match &input_fn.sig.output {
// syn::ReturnType::Type(_, ty) => Some(ty),
// _ => None,
// };
// Detect async function to insert `.await` automatically
let injected = if sig.asyncness.is_some() {
quote! {
let #binding_name = #cache_var.get::<_>(&#key_expr).await;
}
} else {
quote! {
let #binding_name = #cache_var.get::<_>(&#key_expr);
}
};
Ok(quote! {
#vis #sig {
#injected
#block
}
})
}
</file>
<file path="macros/src/from_hashmap.rs">
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type, TypePath};
pub(crate) fn derive_from_hashmap_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let fields = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields
} else {
panic!("FromHashMap can only be derived on structs with named fields.");
}
} else {
panic!("FromHashMap can only be derived on structs.");
};
let field_mappings = fields.named.iter().map(|f| {
let field_name = &f.ident;
let field_str = field_name.as_ref().unwrap().to_string();
let ty = &f.ty;
// vec
if let Type::Path(TypePath { path, .. }) = ty {
let type_name = quote!(#path).to_string();
if type_name.starts_with("Vec <") {
if type_name.contains("Option") {
return quote! {
#field_name: fields.get(#field_str)
.map(|val| val.split(',')
.map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
trimmed.parse().ok()
}
})
.collect::<Vec<Option<_>>>()
).unwrap_or_default(),
};
} else {
return quote! {
#field_name: fields.get(#field_str)
.map(|val| val.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect()
).unwrap_or_default(),
};
}
}
//TODO nested struct
if !["String", "u8", "u16", "i32", "f64", "bool", "Option"]
.iter()
.any(|&t| type_name.contains(t))
{
return quote! {
#field_name: fields.iter()
.filter(|(k, _)| k.starts_with(&(#field_str.to_string() + ".")))
.map(|(k, v)| (k.trim_start_matches(&(#field_str.to_string() + ".")).to_string(), v.clone()))
.collect::<std::collections::HashMap<String, String>>()
.into(),
};
}
}
// Handle specific primitive types
if quote!(#ty).to_string().contains("Option") {
quote! {
#field_name: fields.get(#field_str).cloned(),
}
} else if quote!(#ty).to_string().contains("u8") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<u8>().ok())
.unwrap_or_default(),
}
} else if quote!(#ty).to_string().contains("i32") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<i32>().ok())
.unwrap_or_default(),
}
} else if quote!(#ty).to_string().contains("f64") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<f64>().ok())
.unwrap_or_default(),
}
} else {
quote! {
#field_name: fields.get(#field_str).cloned().unwrap_or_default(),
}
}
});
let expanded = quote! {
impl From<std::collections::HashMap<String, String>> for #struct_name {
fn from(fields: std::collections::HashMap<String, String>) -> Self {
Self {
#(#field_mappings)*
}
}
}
};
TokenStream::from(expanded)
}
</file>
<file path="macros/src/lib.rs">
extern crate proc_macro;
use proc_macro::TokenStream;
mod cache_it;
mod from_hashmap;
#[proc_macro_derive(FromHashMap)]
pub fn from_hashmap_derive(input: TokenStream) -> TokenStream {
from_hashmap::derive_from_hashmap_impl(input)
}
#[proc_macro_attribute]
pub fn cache_it(attr: TokenStream, item: TokenStream) -> TokenStream {
cache_it::cache(attr, item)
}
</file>
<file path="macros/Cargo.toml">
[package]
name = "doless_macros"
version = "0.3.0"
edition = "2021"
description = "A Rust macro to simplify struct mapping and function utilities."
license = "MIT OR Apache-2.0"
repository = "https://github.com/nawajar/DoLess"
keywords = ["macro", "procedural", "struct", "mapping", "timing"]
categories = ["development-tools", "rust-patterns"]
readme = "README.md"
[dependencies]
syn = { version = "2", features = ["full", "extra-traits"] }
quote = "1.0.41"
proc-macro2 = "1.0.101"
proc-macro-error = { version = "1.0.4", default-features = false }
[dev-dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
[lib]
proc-macro = true
</file>
<file path="tests/cache_it_test.rs">
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use doless::cache_it;
use doless_core::cache::Cache;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct User {
id: u32,
name: String,
maybe: Option<String>,
}
#[derive(Clone, Default)]
struct DummyCache {
store: Arc<Mutex<HashMap<String, String>>>,
}
impl DummyCache {
pub fn new() -> Self {
Self {
store: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Cache for DummyCache {
fn get<T: DeserializeOwned + Clone>(&self, key: &str) -> Option<T> {
let guard = self.store.lock().ok()?;
serde_json::from_str(guard.get(key)?).ok()
}
fn set<T: Serialize>(&self, key: &str, value: &T) {
if let Ok(json) = serde_json::to_string(value) {
if let Ok(mut map) = self.store.lock() {
map.insert(key.to_string(), json);
}
}
}
fn set_with<T: Serialize>(&self, key: &str, value: &T, extra: u32) {
//May set extra u32 with cache ttl.
self.set(key, value);
}
}
//
// -- Macro-driven test functions
//
/// 🧩 Default usage — static key, Vec<String> type
#[cache_it(key = "user:list")]
fn get_names(cache: &impl Cache) -> Vec<String> {
let infer_user: Option<Vec<String>> = cache_data;
if let Some(mut users) = infer_user {
users.sort();
return users;
}
let result = vec!["alice".into(), "bob".into()];
cache.set("user:list", &result);
//cache.set_with("user:list", &result, 32);
result
}
/// 🧩 Static key, struct type
#[cache_it(key = "user")]
fn get_user(cache: &impl Cache) -> Option<User> {
cache_data
}
/// 🧩 Custom cache variable name and binding name
#[cache_it(var = redis, key = "user_custom", name = cached_user)]
fn get_user_custom_var(redis: &impl Cache) -> Option<User> {
cached_user
}
/// 🧩 Runtime dynamic key expression
#[cache_it(key = format!("user:{}", id))]
fn get_user_custom_dynamic_key(id: u32, cache: &impl Cache) -> Option<User> {
cache_data
}
//
// -- Helper functions
//
fn default_user(id: u32, name: &str, maybe: Option<&str>) -> User {
User {
id,
name: name.to_string(),
maybe: maybe.map(|s| s.to_string()),
}
}
//
// -- Tests grouped by behavior
//
#[test]
fn test_list_cache_miss_populates_store() {
let cache = DummyCache::new();
let users = get_names(&cache);
assert_eq!(users, vec!["alice".to_string(), "bob".to_string()]);
}
#[test]
fn test_list_cache_hit_returns_existing() {
let cache = DummyCache::new();
let items = vec![String::from("john"), String::from("snow")];
cache.set("user:list", &items);
let users = get_names(&cache);
assert_eq!(users, items, "should return existing cached names");
}
#[test]
fn test_user_cache_miss_returns_none() {
let cache = DummyCache::new();
assert!(
get_user(&cache).is_none(),
"should not find user when cache empty"
);
}
#[test]
fn test_user_cache_hit_returns_data() {
let cache = DummyCache::new();
let user = default_user(1, "jeff", Some("jeffy"));
cache.set("user", &user);
let cached = get_user(&cache).expect("expected cached user");
assert_eq!(cached, user);
}
#[test]
fn test_custom_var_cache_hit() {
let cache = DummyCache::new();
let user = default_user(1, "jane", None);
cache.set("user_custom", &user);
let cached = get_user_custom_var(&cache).expect("expected cached user");
assert_eq!(cached, user);
}
#[test]
fn test_dynamic_key_cache_hit() {
let cache = DummyCache::new();
let user = default_user(2, "peter", None);
let key = format!("user:{}", user.id);
cache.set(&key, &user);
let cached = get_user_custom_dynamic_key(user.id, &cache).expect("expected cached user");
assert_eq!(cached, user);
}
#[test]
fn test_dynamic_key_miss_returns_none() {
let cache = DummyCache::new();
assert!(get_user_custom_dynamic_key(10, &cache).is_none());
}
</file>
<file path=".github/workflows/rust.yml">
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
</file>
<file path=".gitignore">
debug/
target/
Cargo.lock
**/*.rs.bk
*.pdb
</file>
<file path="src/lib.rs">
pub use doless_core::*;
pub use doless_macros::*;
</file>
<file path="repomix-output.txt">
This file is a merged representation of the entire codebase, combining all repository files into a single document.
Generated by Repomix on: 2025-03-01T06:50:30.114Z
================================================================
File Summary
================================================================
Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
a. A separator line (================)
b. The file path (File: path/to/file)
c. Another separator line
d. The full contents of the file
e. A blank line
Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
original repository files, not this packed version.
- When processing this file, use the file path to distinguish
between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
the same level of security as you would the original repository.
Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's
configuration.
- Binary files are not included in this packed representation. Please refer to
the Repository Structure section for a complete list of file paths, including
binary files.
Additional Info:
----------------
================================================================
Directory Structure
================================================================
src/
derived_stuff.rs
from_hashmap.rs
lib.rs
tests/
from_hashmap_test.rs
.gitignore
Cargo.toml
README.md
================================================================
Files
================================================================
================
File: src/derived_stuff.rs
================
use proc_macro::TokenStream;
use quote::quote;
use serde_json;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type, TypePath};
#[cfg_attr(feature = "extra-traits", derive(Debug, Eq, PartialEq, Hash))]
pub(crate) fn derive_stuff_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let fields = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields
} else {
panic!("FromHashMap can only be derived on structs with named fields.");
}
} else {
panic!("FromHashMap can only be derived on structs.");
};
let field_mappings = fields.named.iter().map(|f| {
let field_name = &f.ident;
let field_str = field_name.as_ref().unwrap().to_string();
let ty = &f.ty;
// Handle nested struct types
if let Type::Path(TypePath { path, .. }) = ty {
let type_name = quote!(#path).to_string();
if type_name.starts_with("Vec <") {
return quote! {
#field_name: fields.get(#field_str)
.map(|val| val.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect()
).unwrap_or_default(),
};
}
//TODO nested
if !["String", "u8", "u16", "i32", "f64", "bool", "Option"]
.iter()
.any(|&t| type_name.contains(t))
{
return quote! {
#field_name: fields.iter()
.filter(|(k, _)| k.starts_with(&(#field_str.to_string() + ".")))
.map(|(k, v)| (k.trim_start_matches(&(#field_str.to_string() + ".")).to_string(), v.clone()))
.collect::<std::collections::HashMap<String, String>>()
.into(),
};
}
}
// Handle specific primitive types
if quote!(#ty).to_string().contains("Option") {
quote! {
#field_name: fields.get(#field_str).cloned(),
}
} else if quote!(#ty).to_string().contains("u8") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<u8>().ok())
.unwrap_or_default(),
}
} else if quote!(#ty).to_string().contains("i32") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<i32>().ok())
.unwrap_or_default(),
}
} else if quote!(#ty).to_string().contains("f64") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<f64>().ok())
.unwrap_or_default(),
}
} else {
quote! {
#field_name: fields.get(#field_str).cloned().unwrap_or_default(),
}
}
});
let expanded = quote! {
impl From<std::collections::HashMap<String, String>> for #struct_name {
fn from(fields: std::collections::HashMap<String, String>) -> Self {
Self {
#(#field_mappings)*
}
}
}
};
TokenStream::from(expanded)
}
use quote::ToTokens;
use std::fmt;
struct DebugType<'a>(&'a Type);
impl<'a> fmt::Debug for DebugType<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.to_token_stream())
}
}
fn debug_syn<T: serde::Serialize>(syn_type: &T) {
println!("{}", serde_json::to_string_pretty(syn_type).unwrap());
}
================
File: src/from_hashmap.rs
================
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type, TypePath};
#[cfg_attr(feature = "extra-traits", derive(Debug, Eq, PartialEq, Hash))]
pub(crate) fn derive_from_hashmap_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let fields = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields
} else {
panic!("FromHashMap can only be derived on structs with named fields.");
}
} else {
panic!("FromHashMap can only be derived on structs.");
};
let field_mappings = fields.named.iter().map(|f| {
let field_name = &f.ident;
let field_str = field_name.as_ref().unwrap().to_string();
let ty = &f.ty;
// Handle nested struct types
if let Type::Path(TypePath { path, .. }) = ty {
let type_name = quote!(#path).to_string();
//TODO
if !["String", "u8", "u16", "i32", "f64", "bool", "Option"]
.iter()
.any(|&t| type_name.contains(t))
{
return quote! {
#field_name: fields.iter()
.filter(|(k, _)| k.starts_with(&(#field_str.to_string() + ".")))
.map(|(k, v)| (k.trim_start_matches(&(#field_str.to_string() + ".")).to_string(), v.clone()))
.collect::<std::collections::HashMap<String, String>>()
.into(),
};
}
}
// Handle specific primitive types
if quote!(#ty).to_string().contains("Option") {
quote! {
#field_name: fields.get(#field_str).cloned(),
}
} else if quote!(#ty).to_string().contains("u8") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<u8>().ok())
.unwrap_or_default(),
}
} else if quote!(#ty).to_string().contains("i32") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<i32>().ok())
.unwrap_or_default(),
}
} else if quote!(#ty).to_string().contains("f64") {
quote! {
#field_name: fields.get(#field_str)
.and_then(|val| val.parse::<f64>().ok())
.unwrap_or_default(),
}
} else {
quote! {
#field_name: fields.get(#field_str).cloned().unwrap_or_default(),
}
}
});
let expanded = quote! {
impl From<std::collections::HashMap<String, String>> for #struct_name {
fn from(fields: std::collections::HashMap<String, String>) -> Self {
Self {
#(#field_mappings)*
}
}
}
};
TokenStream::from(expanded)
}
================
File: src/lib.rs
================
extern crate proc_macro;
use proc_macro::TokenStream;
mod from_hashmap;
mod derived_stuff;
#[proc_macro_derive(FromHashMap)]
pub fn from_hashmap_derive(input: TokenStream) -> TokenStream {
from_hashmap::derive_from_hashmap_impl(input).into()
}
/// `DerivedStuff` (for debugging)
#[proc_macro_derive(DerivedStuff)]
pub fn derived_stuff(input: TokenStream) -> TokenStream {
derived_stuff::derive_stuff_impl(input)
}
================
File: tests/from_hashmap_test.rs
================
use doless::{DerivedStuff, FromHashMap};
use std::collections::HashMap;
#[derive(FromHashMap, Debug)]
struct Car {
model: String,
brand: String,
an_option_field: Option<String>,
number: u8,
details: CarDetails,
}
#[derive(FromHashMap, Debug)]
struct CarDetails {
name: String,
description: String,
}
#[derive(DerivedStuff, Debug)]
struct CarDetailsX {
vec_string: Vec<String>,
vec_u8: Vec<u8>,
name: String,
description: String,
//vec_option: Vec<Option<String>>,
}
#[test]
fn test_from_hashmap() {
let mut data = HashMap::new();
data.insert("model".to_string(), "GT-R".to_string());
data.insert("brand".to_string(), "Nissan".to_string());
data.insert("number".to_string(), "8".to_string());
data.insert("details.name".to_string(), "v8engine".to_string());
data.insert("details.description".to_string(), "500hp".to_string());
let car: Car = Car::from(data);
assert_eq!(car.model, "GT-R");
assert_eq!(car.brand, "Nissan");
assert_eq!(car.number, 0b1000);
assert_eq!(car.an_option_field, None);
assert_eq!(car.details.name, "v8engine");
assert_eq!(car.details.description, "500hp");
}
#[test]
fn test_from() {
let mut data = HashMap::new();
data.insert("vec_string".to_string(), "hello, world, rust".to_string());
data.insert("vec_u8".to_string(), "1, 2, 999".to_string());
let car_details: CarDetailsX = CarDetailsX::from(data);
// let x = CarDetailsX{
// vectest: vec![String::from("hello")]
// };
println!("{:?}", car_details)
}
================
File: .gitignore
================
debug/
target/
Cargo.lock
**/*.rs.bk
*.pdb
================
File: Cargo.toml
================
[package]
name = "doless"
version = "0.2.0"
edition = "2021"
description = "A Rust macro to simplify struct mapping and function utilities."
license = "MIT OR Apache-2.0"
repository = "https://github.com/nawajar/DoLess"
keywords = ["macro", "procedural", "struct", "mapping", "timing"]
categories = ["development-tools", "rust-patterns"]
readme = "README.md"
[dependencies]
syn = { version = "2", features = ["full", "extra-traits"] }
quote = "1"
proc-macro2 = "1"
proc-macro-error = { version = "1", default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[lib]
proc-macro = true
================
File: README.md
================
# DoLess - Procedural Macro for Struct Mapping 🦀
`DoLess` is a Rust **procedural macro** that allows structs to be initialized from a `HashMap<String, String>`. It automatically maps field values, providing **type-safe conversions**.
## 🚀 Features
- 🏢 **Auto-implements `From<HashMap<String, String>>`** for structs.
- 🔄 **Supports common Rust types** (`String`, `u8`, `u16`, `i32`, `f64`, `Option`, etc.).
- ❌ **Compile-time errors for unsupported types**.
- ✅ **Default values for missing fields**.
- ⚙ **Supports nested struct parsing** with `.` notation.
---
## 📦 Installation
Add `DoLess` to your `Cargo.toml`:
```toml
[dependencies]
doless = "0.2.0"
```
## 👺 Usage
### Basic Struct Mapping
```rust
use doless::FromHashMap;
use std::collections::HashMap;
#[derive(FromHashMap, Debug, PartialEq)]
struct Car {
model: String,
year: u16,
}
fn main() {
let mut data = HashMap::new();
data.insert("model".to_string(), "GT-R".to_string());
data.insert("year".to_string(), "2023".to_string());
let car: Car = Car::from(data);
println!("Car: Model = {}, Year = {}", car.model, car.year);
}
```
### Nested Struct Support
```rust
use doless::FromHashMap;
use std::collections::HashMap;
#[derive(FromHashMap, Debug)]
struct Car {
model: String,
brand: String,
number: u8,
details: CarDetails, // ✅ Nested Struct Support
}
#[derive(FromHashMap, Debug)]
struct CarDetails {
name: String,
description: String,
}
fn main() {
let mut data = HashMap::new();
data.insert("model".to_string(), "GT-R".to_string());
data.insert("brand".to_string(), "Nissan".to_string());
data.insert("number".to_string(), "8".to_string());
// ✅ Nested Fields with Prefix Notation
data.insert("details.name".to_string(), "Skyline".to_string());
data.insert("details.description".to_string(), "Legendary Sports Car".to_string());
let car: Car = Car::from(data);
println!("{:?}", car);
}
```
### Expected Output
```rust
Car {
model: "GT-R",
brand: "Nissan",
number: 8,
details: CarDetails {
name: "Skyline",
description: "Legendary Sports Car"
}
}
```
---
## 🚀 Why Use DoLess?
- **Simple & Lightweight** — No runtime dependencies, just pure Rust.
- **Declarative API** — Uses procedural macros to generate efficient `From<HashMap<String, String>>` implementations.
- **Type-Safe & Extensible** — Ensures correct conversions and supports nesting.
### ⚙ Roadmap
- [x] Basic primitive types mapping
- [x] Nested struct support
- [ ] Custom conversion support
- [ ] Error handling improvements
---
**Happy coding! ✨**
</file>
<file path="tests/from_hashmap_test.rs">
use std::collections::HashMap;
use doless_macros::FromHashMap;
#[derive(FromHashMap, Debug)]
struct Car {
model: String,
brand: String,
an_option_field: Option<String>,
number: u8,
details: CarDetails,
}
#[derive(FromHashMap, Debug)]
struct CarDetails {
name: String,
description: String,
}
#[derive(FromHashMap, Debug)]
struct VecStruct {
vec_string: Vec<String>,
vec_u8: Vec<u8>,
vec_option: Vec<Option<String>>,
}
#[test]
fn test_from_hashmap() {
let mut data = HashMap::new();
data.insert("model".to_string(), "GT-R".to_string());
data.insert("brand".to_string(), "Nissan".to_string());
data.insert("number".to_string(), "8".to_string());
data.insert("details.name".to_string(), "v8engine".to_string());
data.insert("details.description".to_string(), "500hp".to_string());
let car: Car = Car::from(data);
assert_eq!(car.model, "GT-R");
assert_eq!(car.brand, "Nissan");
assert_eq!(car.number, 0b1000);
assert_eq!(car.an_option_field, None);
assert_eq!(car.details.name, "v8engine");
assert_eq!(car.details.description, "500hp");
}
#[test]
fn test_from_hashmap_vec() {
let mut data = HashMap::new();
data.insert("vec_string".to_string(), "hello, world, rust".to_string());
data.insert("vec_u8".to_string(), "1, 2, 999".to_string());
data.insert("vec_option".to_string(), "1,2,,".to_string());
let car_details: VecStruct = VecStruct::from(data);
assert_eq!(car_details.vec_string.len(), 3);
assert_eq!(car_details.vec_string, vec!["hello", "world", "rust"]);
assert_eq!(car_details.vec_u8, vec![1, 2]); //u8 overflow
assert_eq!(
car_details.vec_option,
vec![Some(String::from("1")), Some(String::from("2")), None, None]
);
}
</file>
<file path="README.md">
# DoLess - Procedural Macro for Struct Mapping 🦀
`DoLess` is a Rust **procedural macro** that allows structs to be initialized from a `HashMap<String, String>`. It automatically maps field values, providing **type-safe conversions**.
## 🚀 Features
- 🏢 **Auto-implements `From<HashMap<String, String>>`** for structs.
- 🔄 **Supports common Rust types** (`String`, `u8`, `u16`, `i32`, `f64`, `Option`, `Vec<T>`, `Vec<Option<T>>`, etc.).
- ❌ **Compile-time errors for unsupported types**.
- ✅ **Default values for missing fields**.
- ⚙ **Supports nested struct parsing** with `.` notation.
---
## 🛆 Installation
Add `DoLess` to your `Cargo.toml`:
```toml
[dependencies]
doless = "0.3.0"
```
## 👺 Usage
### Basic Struct Mapping
```rust
use doless::FromHashMap;
use std::collections::HashMap;
#[derive(FromHashMap, Debug, PartialEq)]
struct Car {
model: String,
year: u16,
}
fn main() {
let mut data = HashMap::new();
data.insert("model".to_string(), "GT-R".to_string());
data.insert("year".to_string(), "2023".to_string());
let car: Car = Car::from(data);
println!("Car: Model = {}, Year = {}", car.model, car.year);
}
```
### Nested Struct Support
```rust
use doless::FromHashMap;
use std::collections::HashMap;
#[derive(FromHashMap, Debug)]
struct Car {
model: String,
brand: String,
number: u8,
details: CarDetails, // ✅ Nested Struct Support
}
#[derive(FromHashMap, Debug)]
struct CarDetails {
name: String,
description: String,
}
fn main() {
let mut data = HashMap::new();
data.insert("model".to_string(), "GT-R".to_string());
data.insert("brand".to_string(), "Nissan".to_string());
data.insert("number".to_string(), "8".to_string());
// ✅ Nested Fields with Prefix Notation
data.insert("details.name".to_string(), "Skyline".to_string());
data.insert("details.description".to_string(), "Legendary Sports Car".to_string());
let car: Car = Car::from(data);
println!("{:?}", car);
}
```
### Support for `Vec<T>` and `Vec<Option<T>>`
```rust
use doless::FromHashMap;
use std::collections::HashMap;
#[derive(FromHashMap, Debug)]
struct ItemCollection {
items: Vec<String>, // ✅ Supports Vec<String>
numbers: Vec<u8>, // ✅ Supports Vec<u8>
optional_items: Vec<Option<String>>, // ✅ Supports Vec<Option<T>>
}
fn main() {
let mut data = HashMap::new();
data.insert("items".to_string(), "apple, banana, orange".to_string());
data.insert("numbers".to_string(), "1,2,3".to_string());
data.insert("optional_items".to_string(), "apple,,orange".to_string()); // Empty string = None
let collection: ItemCollection = ItemCollection::from(data);
println!("{:?}", collection);
}
```
### Expected Output
```rust
ItemCollection {
items: ["apple", "banana", "orange"],
numbers: [1, 2, 3],
optional_items: [Some("apple"), None, Some("orange")],
}
```
---
## 🚀 Why Use DoLess?
- **Simple & Lightweight** — No runtime dependencies, just pure Rust.
- **Declarative API** — Uses procedural macros to generate efficient `From<HashMap<String, String>>` implementations.
- **Type-Safe & Extensible** — Ensures correct conversions and supports nesting.
### ⚙ Roadmap
- [x] Basic primitive types mapping
- [x] Nested struct support
- [x] `Vec<T>` and `Vec<Option<T>>` support
- [ ] Custom conversion support
- [ ] Error handling improvements
---
**Happy coding! ✨**
</file>
<file path="Cargo.toml">
[package]
name = "doless"
version = "0.1.0"
edition = "2021"
[dependencies]
doless_core = { path = "./core" }
doless_macros = { path = "./macros" }
[dev-dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
</file>
</files>