This file is a merged representation of the entire codebase, combined into a single document by Repomix.

================================================================
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. Repository files (if enabled)
5. 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
- 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)


================================================================
Directory Structure
================================================================
.claude/
  settings.local.json
.github/
  workflows/
    rust.yml
core/
  src/
    cache.rs
    lib.rs
  Cargo.toml
  README.md
macros/
  src/
    cache_it.rs
    from_hashmap.rs
    lib.rs
  Cargo.toml
  README.md
src/
  lib.rs
tests/
  cache_it_test.rs
  from_hashmap_test.rs
.gitignore
Cargo.toml
README.md
repomix-output.xml

================================================================
Files
================================================================

================
File: .claude/settings.local.json
================
{
  "permissions": {
    "allow": [
      "Bash(cargo test:*)"
    ],
    "deny": []
  }
}

================
File: .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: 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);
    }
}

#[async_trait::async_trait]
pub trait AsyncCache {
    async fn get<T>(&self, key: &str) -> Option<T>
    where
        T: serde::de::DeserializeOwned + Clone + Send + Sync;

    async fn set<T>(&self, key: &str, value: &T)
    where
        T: serde::Serialize + Send + Sync;

    /// Store with TTL (seconds)
    async fn set_with_ttl<T>(&self, key: &str, value: &T, ttl_secs: u64)
    where
        T: serde::Serialize + Send + Sync;
}

================
File: core/src/lib.rs
================
pub mod cache;

================
File: core/README.md
================
# DoLess core

`DoLess core` trait for support macro.

================
File: 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: 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: 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: macros/README.md
================
# DoLess macro

`DoLess macro` macro.

================
File: tests/cache_it_test.rs
================
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};

use doless::{cache::AsyncCache, 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);
    }
}

pub struct RedisCache {
    store: Arc<Mutex<HashMap<String, String>>>,
}
impl RedisCache {
    pub fn new() -> Self {
        Self {
            store: Arc::new(Mutex::new(HashMap::new())),
        }
    }
}

impl Default for RedisCache {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait::async_trait]
impl AsyncCache for RedisCache {
    async fn get<T>(&self, key: &str) -> Option<T>
    where
        T: DeserializeOwned + Clone + Send + Sync,
    {
        let guard = self.store.lock().ok()?;
        serde_json::from_str(guard.get(key)?).ok()
    }

    async fn set<T>(&self, key: &str, value: &T)
    where
        T: Serialize + Send + Sync,
    {
        if let Ok(json) = serde_json::to_string(value) {
            if let Ok(mut map) = self.store.lock() {
                map.insert(key.to_string(), json);
            }
        }
    }

    async fn set_with_ttl<T>(&self, key: &str, value: &T, ttl_secs: u64)
    where
        T: Serialize + Send + Sync,
    {
        //TODO
        self.set(key, value).await;
    }
}

//
// -- 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
}

/// 🧩 async
#[cache_it(key = "user")]
async fn get_user_async(cache: &impl AsyncCache) -> 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());
}

#[tokio::test]
async fn test_async_user_cache_hit_returns_data() {
    let cache = RedisCache::new();
    let user = default_user(1, "jeff", Some("jeffy"));
    cache.set("user", &user).await;

    let cached = get_user_async(&cache).await.expect("expected cached user");
    assert_eq!(cached, user);
}

================
File: .gitignore
================
debug/
target/
Cargo.lock
**/*.rs.bk
*.pdb

================
File: repomix-output.xml
================
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>

================
File: macros/Cargo.toml
================
[package]
name = "doless_macros"
version = "0.4.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: core/Cargo.toml
================
[package]
name = "doless_core"
version = "0.4.1"
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]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
async-trait = "0.1"

================
File: src/lib.rs
================
pub use doless_core::*;
pub use doless_macros::*;

================
File: 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: README.md
================
# DoLess 🦀 — Procedural Macros for Data Mapping and Caching

`DoLess` is a Rust library offering **procedural macros** that simplify both
**data-to-struct mapping** and **cache integration** patterns.

It provides two main features:

- 🧩 **`#[derive(FromHashMap)]`** — auto-generates a type-safe
  `From<HashMap<String, String>>` implementation for simple and nested structs.
- ⚡ **`#[cache_it(...)]`** — injects cache lookup logic directly into your functions.

---

## 🚀 Features

### 🧩 Mapping Features
- ✅ Auto-maps from `HashMap<String, String>`
- 🔢 Supports types: `String`, numeric primitives, `bool`, `Option<T>`
- ➕ Supports lists: `Vec<T>`, `Vec<Option<T>>`
- 🪆 Nested structs with dot notation (`details.name`)
- ⚙ Defaults for missing fields

### ⚡ Cache Macro Features
- 📦 Add `#[cache_it(...)]` to perform cache lookups automatically
- 🗝 Configurable options:
  - `key = "some:key"`
  - `key = format!("user:{}", id)`
  - `var = redis` — custom cache instance name
  - `name = cached_data` — custom binding name
- 🔄 Works with any cache backend implementing the `Cache` trait
- 🚀 Async-aware — supports async functions automatically

---

## 🧠 Design Intent: Lookup-Only by Default

The `#[cache_it]` macro performs **non-intrusive cache lookups**:

```rust
let cache_data = cache.get::<_>(&key);
```

That’s it — no automatic writes.  
You decide when to cache the result:

- ✅ Cache only successful responses  
- ✅ Skip caching transient or sensitive data  
- ✅ Apply your own TTLs or invalidation logic  

> This intentional design keeps `DoLess` flexible and predictable.  
> You’re in control of every cache write.

---

## 📦 Installation

```toml
[dependencies]
doless = "0.4.1"
```

Includes:
- `doless_core` — Cache trait, shared utilities
- `doless_macros` — Procedural macros
- `doless` — Public re-export crate

---

## ✨ Usage Guide

### 🧩 FromHashMap Example

```rust
use doless::FromHashMap;
use std::collections::HashMap;

#[derive(FromHashMap, Debug)]
struct Car {
    model: String,
    brand: String,
    details: CarDetails,
    tags: Vec<String>,
}

#[derive(FromHashMap, Debug)]
struct CarDetails {
    name: String,
    description: String,
}

fn main() {
    let mut data = HashMap::new();
    data.insert("model".into(), "GT-R".into());
    data.insert("brand".into(), "Nissan".into());
    data.insert("details.name".into(), "Skyline".into());
    data.insert("details.description".into(), "Legendary Sports Car".into());
    data.insert("tags".into(), "fast,collectible,cool".into());

    let car: Car = Car::from(data);
    println!("{:#?}", car);
}
```

---

### ⚡ Cache Macro Example

#### Step 1: Implement the `Cache` Trait

```rust
use doless_core::cache::Cache;
use serde::{de::DeserializeOwned, Serialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;

#[derive(Clone, Default)]
struct DummyCache {
    store: Arc<Mutex<HashMap<String, String>>>,
}

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) {
        // Default: fallback to simple `set`
        // `extra` can be interpreted as TTL (in seconds) in real caches
        println!("Setting with TTL={extra}s");
        self.set(key, value);
    }
}
```

---

#### Step 2: Lookup Function with Optional Write

```rust
use doless::cache_it;
use doless_core::cache::Cache;

#[cache_it(key = "user:list")]
fn get_users(cache: &impl Cache) -> Vec<String> {
    // 👇 `cache_data` is automatically generated by the macro
    let cache_data: Option<Vec<String>> = cache_data;

    // Return directly if cache hit
    if let Some(users) = cache_data {
        return users;
    }

    // Miss: compute, store, and return
    let result = vec!["alice".into(), "bob".into()];
    cache.set("user:list", &result);
    result
}
```

---

### Dynamic Key Example

```rust
#[cache_it(key = format!("user:{}", id))]
fn get_user(id: u32, cache: &impl Cache) -> Option<User> {
    cache_data
}
```

### Custom Variable Names

```rust
#[cache_it(var = redis, key = "session:active", name = cached_session)]
fn get_active_session(redis: &impl Cache) -> Option<Session> {
    cached_session
}
```

---

## ⏱ Extending Cache: TTL and Custom `set_with`

In `DoLess`, the `Cache` trait supports an overridable method for extra control:

```rust
fn set_with<T: Serialize>(&self, key: &str, value: &T, extra: u32) {
    // Use `extra` for TTL, versioning, etc.
    self.set(key, value);
}
```

You can override it to implement **time-to-live (TTL)** or other advanced behaviors.

Example extended cache:

```rust
use std::time::{Duration, Instant};

struct TTLCache {
    data: Arc<Mutex<HashMap<String, (String, Instant)>>>,
}

impl Cache for TTLCache {
    fn get<T: DeserializeOwned + Clone>(&self, key: &str) -> Option<T> {
        let mut guard = self.data.lock().ok()?;
        let (json, expiry) = guard.get(key)?.clone();

        // Remove expired entries
        if Instant::now() > expiry {
            guard.remove(key);
            return None;
        }

        serde_json::from_str(&json).ok()
    }

    fn set<T: Serialize>(&self, key: &str, value: &T) {
        self.set_with(key, value, 60);
    }

    fn set_with<T: Serialize>(&self, key: &str, value: &T, ttl_secs: u32) {
        if let Ok(json) = serde_json::to_string(value) {
            let expiry = Instant::now() + Duration::from_secs(ttl_secs as u64);
            if let Ok(mut map) = self.data.lock() {
                map.insert(key.to_string(), (json, expiry));
            }
        }
    }
}
```

Now you can call:

```rust
cache.set_with("user:1", &user, 120); // cache for 2 minutes
```

---

## 🧪 Testing

Full examples and integration tests live under:
- [`tests/cache_it_test.rs`](./tests/cache_it_test.rs)
- [`tests/from_hashmap_test.rs`](./tests/from_hashmap_test.rs)

Run with:
```bash
cargo test
```

---

## 🧭 Roadmap

| Feature                                |   Status   |
| -------------------------------------- | :--------: |
| FromHashMap with nested struct support |     ✅      |
| Vec and Vec<Option<T>> handling        |     ✅      |
| Cache attribute macro                  |     ✅      |
| Async auto-detection                   |     ✅      |
| TTL + extended cache (via set_with)    | ✅ (manual) |
| Custom derive hooks                    | 🚧 Planned  |
| Error diagnostics                      | 🚧 Planned  |

---

## 📦 Project Structure

| Crate           | Purpose                                            |
| --------------- | -------------------------------------------------- |
| `doless_core`   | Core cache trait and foundational types            |
| `doless_macros` | Macro implementations for data mapping and caching |
| `doless`        | Unified public API layer                           |

---

**DoLess — Write Less, Do More.**

🦀 Rust procedural macros that make your data + cache handling simpler, safer, and smarter.

================
File: Cargo.toml
================
[package]
name = "doless"
version = "0.4.1"
edition = "2021"
description = "A Rust macro to simplify struct mapping , injects cache lookup logic directly into your functions ,and function utilities."
license = "MIT OR Apache-2.0"
repository = "https://github.com/nawajar/DoLess"
keywords = [
    "macro",
    "struct",
    "mapping",
    "timing",
    "cache",
]
categories = ["development-tools", "rust-patterns"]
readme = "README.md"
[dependencies]
# doless_core = { version = "0.4.0" }
# doless_macros = { version = "0.4.0" }

doless_core = { path = "./core" }
doless_macros = { path = "./macros" }

[dev-dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
async-trait = "0.1"
tokio = { version = "1.43.0", features = ["full"] }




================================================================
End of Codebase
================================================================
