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
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.xml

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

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

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

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

================
File: 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: 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/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: 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: 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: .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: .gitignore
================
debug/
target/
Cargo.lock
**/*.rs.bk
*.pdb

================
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 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: 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"




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