objs 0.1.0

Lightweight macros for creating typed, or untyped, named, or anonymous objects in Rust.
Documentation
  • Coverage
  • 100%
    4 out of 4 items documented4 out of 4 items with examples
  • Size
  • Source code size: 44.83 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 323.25 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 2s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • romishalom

objs

Crates.io Docs.rs License License

objs is a small, zero-dependency Rust macro library for creating named and unnamed (anonymous) objects on the fly — without hand-writing a struct declaration every time you need a quick, ad-hoc bundle of fields.

It's useful for test fixtures, one-off "record" values, lightweight configuration objects, and anywhere you'd reach for an anonymous struct or object literal in other languages, but want to stay in idiomatic, zero-cost Rust.

use objs::obj;

let point = obj! {
    x: i32 = 10,
    y: i32 = 20,
};

assert_eq!(point.x, 10);
assert_eq!(point.y, 20);

Under the hood, every macro in this crate expands to a completely ordinary Rust struct. There's no runtime overhead, no dynamic typing, and no hidden allocations — you get a real, statically-typed value that the compiler treats exactly like any struct you'd write by hand.


Table of contents


Installation

Add objs to your Cargo.toml:

[dependencies]

objs = "0.1"

All three macros are exported from the crate root, so you can bring in exactly what you need:

use objs::{obj, infer_obj, let_obj};

Overview

Macro Produces Type name Types Supports Attributes Supports impl Reusable type?
obj! Anonymous object Object (block-scoped) Explicit, per-field ✅ (before fields) ❌ (scoped to the macro's block)
infer_obj! Anonymous object Object (block-scoped, generic) Inferred from values ✅ (before fields) ❌ (scoped to the macro's block)
let_obj! Named object + variable Whatever name you choose Explicit, per-field ✅ (before object name)

obj! — anonymous, explicitly-typed objects

obj! builds a one-off object with explicitly annotated field types. It expands to a locally-scoped struct Object { .. } plus an initialized instance of it, all wrapped in a block expression.

Syntax

obj! {
    [#[attributes]]
    field_name: FieldType = value,
    ...
    [; impl [Trait] { .. } ...]
}
  • Fields are comma-separated name: Type = value pairs. A trailing comma is optional, and the field list may be empty.
  • Optional attributes (like #[derive(Debug)]) may precede the field list and are forwarded onto the generated struct.
  • After a ;, you may add one or more impl [Trait] { .. } blocks to give the object methods or trait implementations.

Examples

Basic usage:

use objs::obj;

let point = obj! {
    x: i32 = 10,
    y: i32 = 20,
};

assert_eq!(point.x, 10);
assert_eq!(point.y, 20);

Implementing a trait:

use objs::obj;
use std::fmt;

let person = obj! {
    name: String = "Alice".to_string(),
    age: u32 = 30;
    impl fmt::Display {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{} is {}", self.name, self.age)
        }
    }
};

assert_eq!(format!("{}", person), "Alice is 30");

Adding inherent methods with a bare impl { .. } block (no trait):

use objs::obj;

let calculator = obj! {
    factor: i32 = 2;
    impl {
        fn multiply(&self, val: i32) -> i32 {
            val * self.factor
        }
    }
};

assert_eq!(calculator.multiply(5), 10);

An empty object is valid too:

use objs::obj;

let empty = obj! {};
drop(empty);

Note: the generated struct is always named Object internally, but since the whole thing is wrapped in a block expression, this never collides with other obj! calls elsewhere in your code. You just can't refer to that type by name from outside the block — if you need that, use let_obj! instead.


infer_obj! — anonymous, type-inferred objects

infer_obj! is like obj!, but you skip writing out field types entirely. Each field becomes its own generic type parameter on the generated struct, and Rust infers the concrete type from the value you assign. This is handy for quick, throwaway objects where explicit types would just be noise.

Syntax

infer_obj! {
    [#[attributes]]
    field_name: value,
    ...
}
  • Fields are comma-separated name: value pairs. A trailing comma is optional, and the field list may be empty.
  • Optional attributes may precede the field list.

Examples

use objs::infer_obj;

let mixed = infer_obj! {
    integer: 42,
    float: 3.14,
    text: "Hello",
};

assert_eq!(mixed.integer, 42);
assert_eq!(mixed.float, 3.14);
assert_eq!(mixed.text, "Hello");

Objects can be nested, since each field's value is just an ordinary expression:

use objs::infer_obj;

let complex = infer_obj! {
    id: 1,
    nested: infer_obj! {
        inner_val: "nested",
    },
};

assert_eq!(complex.id, 1);
assert_eq!(complex.nested.inner_val, "nested");

An empty object is valid too:

use objs::infer_obj;

let empty = infer_obj! {};
drop(empty);

Note: infer_obj! does not support impl blocks — because the generated struct is generic over every field's type, implementing a trait or method on it would require naming those generic parameters yourself, which defeats the purpose of the macro. If you need behavior attached to the object, reach for obj! or let_obj! instead. Also note that each field name doubles as a generic type parameter name, so field names must be valid, unique identifiers.


let_obj! — named objects bound to a variable

let_obj! declares a real, nameable struct and binds an instance of it to a local variable — with the same name as the type — in a single step. Unlike obj! and infer_obj!, the generated type is left in scope, so you can construct additional instances of it later using normal struct-literal syntax.

Syntax

let_obj! {
    [#[attributes]]
    [mut] TypeName {
        field_name: FieldType = value,
        ...
    }
    [impl [Trait] { .. }]...
}
  • TypeName becomes both the struct's type name and the bound variable's name.
  • The optional mut keyword makes the variable mutable.
  • Fields are comma-separated name: Type = value pairs, with an optional trailing comma. The field list may be empty.
  • Optional attributes may precede TypeName and are forwarded to the struct definition.
  • Zero or more impl [Trait] { .. } blocks may follow the field list.

Examples

Immutable object:

use objs::let_obj;

let_obj! {
    User {
        id: u64 = 1001,
        active: bool = true,
    }
};

assert_eq!(User.id, 1001);
assert_eq!(User.active, true);

Mutable object:

use objs::let_obj;

let_obj! {
    mut Counter {
        count: i32 = 0,
    }
};

assert_eq!(Counter.count, 0);
Counter.count += 5;
assert_eq!(Counter.count, 5);

Implementing a trait, then reusing the generated type by name:

use objs::let_obj;

trait Greeter {
    fn greet(&self) -> String;
}

let_obj! {
    Bot {
        name: &'static str = "Robo",
    }
    impl Greeter {
        fn greet(&self) -> String {
            format!("Hello from {}", self.name)
        }
    }
};

assert_eq!(Bot.greet(), "Hello from Robo");

// `Bot` the type is still in scope, so it can be constructed again
// explicitly, just like any other struct.
let another_bot: Bot = Bot { name: "Android" };
assert_eq!(another_bot.name, "Android");

An empty object is valid too:

use objs::let_obj;

let_obj! {
    EmptyObj {}
};

let _copy = EmptyObj;

Note: because the generated variable shares its name with the generated type, let_obj! applies #[allow(non_camel_case_types)] and #[allow(non_snake_case)] internally, so using a conventional PascalCase type name as a variable won't trigger lint warnings.


Choosing the right macro

  • Need a quick, throwaway value and don't care about the type's name? Use obj! (or infer_obj! if you don't want to write out field types).
  • Need to attach a trait implementation or methods, but only within a single expression/block? Use obj!.
  • Need the type itself to be reusable — as a function parameter, return type, or to construct more instances later? Use let_obj!.
  • Want the absolute least ceremony, with field types inferred instead of written out? Use infer_obj!.

Attributes and derives

All three macros forward attributes placed before the field list onto the generated struct, so standard derives work as expected:

use objs::obj;

let tagged = obj! {
    #[allow(dead_code)]
    secret: i32 = 99,
};

assert_eq!(tagged.secret, 99);

Implementing traits and methods

Both obj! and let_obj! support trailing impl blocks:

  • impl { .. } implements inherent methods directly on the generated object.
  • impl SomeTrait { .. } implements SomeTrait for the generated object.

infer_obj! does not support impl blocks, since its generated struct is generic over every field's type (see above).

Limitations

  • obj! and infer_obj! types are not nameable. Since both expand to a struct named Object inside a block expression, you cannot refer to that exact type from outside the macro invocation. Use let_obj! if you need a type you can name elsewhere (as a function signature, stored field, etc.).
  • infer_obj! field names must be valid generic parameter identifiers, since each field name is reused as a generic type parameter on the generated struct.
  • infer_obj! cannot implement traits or methods, for the same generic-parameter reason described above.
  • These macros generate real structs at the call site, so normal Rust visibility, ownership, and lifetime rules apply — there is no dynamic typing or reflection involved.

License

Licensed under the Apache License and the MIT License. See LICENSE-APACHE2.txt and LICENSE-MIT.txt for details.