ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.75k stars 2.48k forks source link

Tags #1099

Open BraedonWooding opened 6 years ago

BraedonWooding commented 6 years ago

This was discussed originally here, and elsewhere :).

Motivation

I've been looking at implementing lambda functions (by first making all functions anonymous) but I feel like I should begin with something a little easier as to help me understand the codebase; I've recently had need for reflection metadata in a codegen which generates getters/setters (amongst other things) for structs in relation to a stack based system for a markup language that translates to a IR/bytecode representation, anyways it would be nice to allow users to disable/hide this generation with a hide tag (as well as customise such as link getters/setters to other functions). Amongst other tags, regardless this would be a nice feature I'm sure many authors would enjoy. I'll be looking towards implementing this, this upcoming weekend but I wanted to gather opinion on syntax :).

Proposal

The original proposal was this;

const Foo = struct {
    x: u32 @("anything") @(bar()),
};

fn bar() i32 {
    return 1234;
}

However this proposes a problem of how one would access the tags, since they aren't purely the same type; in the majority of cases (specifically I can't think of a case where this isn't true) you just want a string tag not a 'boolean' tag since in reality having something like @(true) is meaningless compared to @("Hide") so I propose the following restriction; all tags are just strings and @("X") is replaced with @tag("X") this makes it a little simpler to read and a lot simpler to actually handle.

const Foo = struct {
  x: u32 @tag("A") @tag("B"),
};

Also tags will be able to be applied to almost anything that is globally scoped that is functions (global), structs (note: variables can't have tags) having something like;

fn X() void @tag("MyFunc") {
    ...
}

You would access it like;

// For Struct
for (@typeInfo(Foo).Struct.fields) |field| {
    for (field.Tags) |tag| {
       // Do whatever with tag
    }
}

// For function
for (@typeInfo(X).Fn) |tag| {
    // Do whatever with tag
}

You could also query all objects that have a certain tag like;

comptime const query = @withTag("A"); // returning typeInfo

Now this would look through each typeInfo available returning a static array of all that have it, however we could also maintain a map of each tag to a list of typeinfo making this much much more efficient but increase the size of the running compiler (I feel a valid tradeoff).

Actual Changes

bheads commented 6 years ago

I was just thinking that I could use attributes the other day, there are uses for more complicated tags. Ex: you could tag members of a struct for serialization: name, type, optional, required flags ect..

ghost commented 6 years ago

I'm not sure I understand the motivation for tags. Could you give a written, practical example maybe?

after all I see this sounds easy for you and you want to continue working on harder topics which is great 😃

thanks

PavelVozenilek commented 6 years ago

I do not see where this would be more useful than confusing. Java has (or had) something similar, and it was big mess to use. Project guidelines/unwritten rules pushed people to place annotations everywhere, no matter how silly it was. Most even didn't know why they were doing it.

Invisibly generated things are evil. For example, serialization is really better to be written down manually, instead of relying on black magic.

If there's area where invisibly generated trickery may be of some use, it would be "aspects", and even this mostly to add debugging checks. Aspects, fortunately, do not require tagging things.


However, there's one specific case where tags should be allowed, and available for compile time and runtime inspection: in tests. My proposals: #567 and #1010. Also the "this test must fail to compile" feature (#513) could be supported in the same way.

BraedonWooding commented 6 years ago

@bheads what's a use-case for a required flag, couldn't you just state that if the flag isn't given it is false by default and if it is given it is true? Things like names and so on can be implemented maybe even like an arg parser like @tag("SetName:Bar"), the important thing is to keep it simple because else it gets really difficult to use because the types don't match and because annoying to go through and iterate tags.

@monouser7dig a good example would be something like;

const OptionsSpec = struct { 
  h: ?bool @tag("flag"),
  fileName: []u8 const,
  inputFiles: [][]u8 const @tag("many"),
  state: i32 @tag("Only: 1, 0, -1"),
};

Another good use case would be having just a simple @tag("Hide") flag to hide a certain field from serialisation.

BraedonWooding commented 6 years ago

@PavelVozenilek I definitely understand your plight with Java's annotations, but keep in mind this ISN'T annotations, this is merely extra meta data. For example in java you have stuff like @throws, @doesThisWeirdThing and so on to describe how something behaves, in this tag system it would ONLY be used for allowing the user to do comptime reflection to perform certain actions such as setup serialisation scripts (as above), and perhaps you want to put all functions that have the @tag("Console") into a dictionary for runtime access so you would have something like;

fn ClearScreen() void @tag("Console") { .. }

This way you would be able to add all the entire the dictionary with a simple for loop using the query function INSTEAD of having;

loadConsoleCommand(Clear);
loadConsoleCommand(Other);
loadConsoleCommand(A);
loadConsoleCommand(B);
loadConsoleCommand(C);

Not having 200 lines of 'loads' and just having a simple for loop is a HUGE thing, I've done this so many times in games that it really is an important use-case :).

So while I understand your problem with Java, I kind of really want to enforce that the intention is not at all aligned with java and much more with C# attributes (have a look at them if you have time) which are ONLY ever used when the user wants to have a unique attribute for stuff like the function thing I gave above, OR when doing certain stuff like serialization.

So I don't see people doing ANY stuff like the Java annotation system, as the only use-case in the std library I can see right now would be for JSON serialization of structs.

ghost commented 6 years ago

okay so a tag is essentially just a string, I'd agree that this is actually easy to read.

I see the point for serialization or the game example..although in the game example I think a different overall data structure may be the real answer but I've not dealt with the same situation myself so could be off.

The @tag("Only: 1, 0, -1") should really be an enum or in other cases a custom type (-> enforced by compiler) though and this @tag("many") does not really contain any meaning?.

what confused me in the beginning was this part @(bar()

const Foo = struct {
   x: u32 @("anything") @(bar()),
                         ^~~~
};

what was this supposed to do in the old proposal?

BraedonWooding commented 6 years ago

@monouser7dig we needed a string to function dictionary because we wanted to allow users to type commands and have them run, maybe there was a better data structure regardless there was a need to have a tag based system.

The bar was to do with the old proposal (under the Clap library issues).

Keep in mind that both of those are context sensitive, they make sense in relation to the library for example the clap library allows you to specify that something is a 'many' argument i.e. is like -s a -s b -s c and this was just chosen to simulate that; a better name could be chosen but I was just trying to keep to that. The case of Only actually maybe indicates the sufferings of using a single type ([]u8) to represent all situations as yes a better case would be enum but how they can be implemented efficiently on the builtin side is the difficult position, unless we want something like;

struct Tag {
    ptr: usize,
    T: type,
}

Which would allow you to ptr cast it back into the original value, however this has problems of allocation and requiring unsafe ptr semantics.

If you can think of a way to have multiple types in such a way that is safe, the only other way I can think of it is have Tag look something like;

struct Tag {
    name: []const u8,
    data: []const u8,
}

You could use the data field to do things like store a float, or an enum or anything and thus the syntax would be; @tag("only", i32(-1), i32(0), i32(1)), or something like that, the data field could even represent anything really a float or whatever; we would take in the arguments as var args storing each argument in the data array, which would be 4*3 (12) bytes; this would be annoying to handle however and is very prone to errors and I don't particularly like this.

I think perhaps the best way to approach this is just encourage string tags that are like hide or many and discourage ones like only: -1, 0, 1. Then later on (or now) if someone has a better idea to allow for the extra types then that can be implemented.

tgschultz commented 6 years ago

I've spent some time thinking about this, and I think it might not even be necessary to introduce a new builtin for most cases. Consider that Zig has a zero-sized void type.

const Foo = struct {
    x: u32,    _note_anything: void, _func_bar: void,

};

fn bar() i32 {
    return 1234;
}

Now Foo will have these names in @typeInfo(Foo).Struct.Fields and we can identify them by their void type and leading _. They can be associated to the x field by their ordering. One issue with this method is that you can't have two fields named identically, so this kind of thing has to happen:

const Pos = struct {
    x: f32, _x_pack_fp2dot14: void, //or _pack_fp2dot14_0
    y: f32, _y_pack_fp2dot14: void,  //or _pack_fp2dot14_1
};

This could potentially be remedied by allowing field names of void type to be repeated, but I'm not sure what consequences might result elsewhere from that. This also couldn't be used to annotate things that aren't fields (although it does make multiple tags simple).

So from there, my thinking is that what we actually want to annotate is not the field itself, but the type of the field. Currently type aliases have no meaning, they are just different names for the same thing. If we could, at comptime, determine what alias was used for a type then we can just operate on that.

const fp2dot14 = f32;

const Pos = struct {
    .x: fp2dot14,
    .y: fp2dot14,
};

Now in comptime code I could look at the alias with something like @aliasNameOf(T) and know how to serialize the field for use in the netcode. Still no way to apply it to a function though, since we can declare a type alias for a function type, but we can't define a function using it.

My personal preference is not to add a builtin if it can be reasonably avoided, but if we did do that, I propose a small change to the syntax presented:

const Pos = struct {
    .x: @tag(f32, "fp2dot14"),
    .y: @tag(f32, "fp2dot14"),
};

//or we can make a type alias of the tagged type:
const u32be = @tag(u32, builtin.Endian.Big);

//and have multiple tags
const u12be_lsb = @tag(u12, builtin.Endian.Big, BitPacking.LSBFirst);

//functions don't quite work the same, but:
fn add(x: i32, y: i32) i32 {
    return x + y;
}
@fnTag(add, Console.TwoArgs);
Hejsil commented 6 years ago

It was discussed in #676, that we could allow _ as a name in structs which could be used for padding and such. That, together with zero sized types, could be used for everything tags would do:

const S = struct {
    _: json.Ignore,
    ignore_me: u8,

    _: json.CustomFormatter(formatEnum),
    some_enum_field: E,
};

We can ever expand this to all definitions if we allow _ in global scope.

const _ = ConsoleCommand{};
fn someConsoleCommand(c: *Console) void {
    c.print("Hello World\n");
}

With #1047, the @withTag could even be implemented in userland, though it would only operate on type instead of the whole project (which I don't think is realistically possible anyway).

// meta.zig
pub fn withTag(comptime Namespace: type, comptime TagType: type) []const TypeInfo.Definition {
    ...
}

// commands.zig
const commands = comptime meta.withTag(this, ConsoleCommand);
tgschultz commented 6 years ago

I knew I just hadn't spent enough time thinking about it. It didn't occur to me that empty structs are also size zero.

bheads commented 6 years ago

I was thinking something like this:

something like json_required would be a parsing error if not found, or json_option is okay is missing json_id would change the mapping, json_type changes parsing types, ect...

const RpcRequest = struct {
    id: u64 @tag("json_required") @tag("json_id", "request id") @tag("json_type", "string"),
    method_name: []u8,
    args: [][]u8, @tag("json_optional")
};

...
const rpc_request = try json.parse(RpcRequest, some_json_string);
thejoshwolfe commented 6 years ago

I don't like the idea of using empty fields to convey something that isn't related to fields. That doesn't convey intent very well.

I was recently confused by C++ code that used std:: piecewise_construct as a parameter to a function, even though it doesn't pass any data to the function. A parameter should pass data, and a field should hold data. That's the intent of those constructs.

ghost commented 6 years ago

@BraedonWooding

The bar was to do with the old proposal (under the Clap library issues).

is this something that would be impossible now? I wanted to ask because maybe the current way would miss that (important?) use case.

bheads commented 6 years ago

Using _ to pack data is interesting but relies on someone just knowing that these exists and its not really a solid pattern for tagging fields.

alexnask commented 6 years ago

I also prefer a userspace solution to this problem.
Something roughly along the lines of:

const Any = struct {
    const Self = this;

    _type: type,
    value: *const u8,

    fn make(val: var) Self {
        return Self { ._type=@typeOf(val), .value=@ptrCast(*const u8, &val), };
    }

    fn get(self: *const Self, comptime T: type) T {
        // TODO: Could allow some conversions here, e.g. from [N]T -> []T, []T -> []const T, *T -> *const T, etc.
        if (T != self._type) {
            @panic("Wrong type.");
        }

        return @ptrCast(*const T, self.value).*;
    }
};

pub const Tag = struct {
    const Self = this;

    name: []const u8,
    value: Any,

    pub fn make(name: []const u8, val: var) Self {
        return Self { .name=name, .value=Any.make(val), };
    }

    pub fn get(self: *const Self, comptime T: type) T {
        return self.value.get(T);
    }
};

pub const TagList = struct {
    pub fn make(tuple: var) this {
        // ...
    }
};

// Checks that the Tags only contain existing fields etc.
pub fn tag_check(comptime T: type) void {
    // ...
}

pub fn has_tags(comptime T: type, comptime field: []const u8) bool {
    // ...
}

pub fn tagsOf(comptime T: type, comptime field: []const u8) []Tag {
    // ...
}

pub fn tagOf(comptime T: type, comptime field: []const u8, comptime tag: []const u8) ?Tag {
    // ...
}

pub const RpcRequest = struct {
    // With proposed tuple syntax
    pub const Tags = TagList.make([
        "id", Tag.make("json_required", {}), Tag.make("json_id", "request id"), Tag.make("json_type", "string"),
        "args", Tag.make("json_optional", {}),
    ]);

    id: u64,
    method_name: []u8,
    args: [][]u8,
};

pub fn json_parse(comptime T: type, data_stream: InputStream) T {
    comptime tag_check(T);

    var instance: T = undefined;

    inline for (@typeInfo(T).Struct.fields) |f| {
        comptime const is_required = tagOf(T, f.name, "json_required") != null;
        // etc etc.
        @field(instance, f.name) = read_some_json(f.field_type, data_stream, is_required, ...);
    }

    return instance;
}
Hejsil commented 6 years ago

@thejoshwolfe I agree that intent is not clear.

I'd say, if we're going to have tags as a language feature, then it should not use strings.

@BraedonWooding proposed storing the type and a data ptr in the tag, and I think that is the best idea. As for allocation, it is really not an issue, as the compiler already stores the data behind const pointers somewhere without the user needing to worry about allocation.

const CustomTag = struct {
    a: u8,
    b: []const u8,
};
const S = struct {
    // I prefer tags to be prefixed just like 'pub' and those.
    // This also makes the language easier to parse
    @tag(CustomTag{ .a = 2, .b = "Hello World" })
    a: u8,

    @tag(CustomTag{ .a = 4, .b = "Hello b" })
    fn b() void {}
};

test "" {
    inline for (@typeInfo(S).Struct.fields) |field| {
        const TagT = field.tag.T;
        const tag = @ptrCast(*const TagT, field.tag.data).*;
        if (TagT == CustomTag) {
            // Do stuff with fields of tag
        }
    }
}
bheads commented 6 years ago

@Hejsil Like this even better

BraedonWooding commented 6 years ago

Wow! Amazing feedback. Most of the proposed 'userland' solutions can be expressed honestly as a bit 'ugly' with @alexnask's probably the least ugly though in that essence it is the least 'tag' like of all of them. I personally prefer the one @Hejsil brought up again (which is honestly is a better derivative of the other idea I had). I think that the issue is quite an important one so it is quite necessary to make it easy to use and not overcomplicated.

My only concern is the requirement for a ptrcast though I feel that is less relevant as this is purely compile time code. I'll draft something up this weekend :). I'll do the prefix method as it is easier to code (no changes to the parser if my memory serves me correct), this will just be a first iteration since this idea has quite a bit of contention (often a physical 'variant' helps isolate where the problems are)

alexnask commented 6 years ago

@BraedonWooding

The type erasure code I posted in that comment seems to work in comptime (I did just a couple of tests), so something similar would be fine I think.

tgschultz commented 6 years ago

Pre/postfix @tag() feels very un-Zig-like to me. It doesn't seem to fit well with the rest of the language. All other builtin functions act like functions, but the proposed @tag() doesn't.

For example:

id: u64 @tag("json_required") @tag("json_id", "request id") @tag("json_type", "string"),

What is @tag() here? It isn't a type, which is the only thing expected between the field name and the comma.

args: [][]u8, @tag("json_optional")

If one assumes the above isn't a typo, you'd expect @tag() to be a new definition of some kind.

@tag(CustomTag{ .a = 2, .b = "Hello World" })
    a: u8,

There's also no precedent for this.

Personally, I still feel like userland comptime solutions that don't add new things to the language are a better way to go, but if we're going to add to the language I feel like it should fit better.

Aside from my own suggestion of making @tag() an actual function that returns a type with annotations in its @typeInfo() (which admittedly raises questions regarding inference and type comparisons), it could be a keyword like const or fn, and use the angle bracket syntax:

const Thing = struct {
    a: tag<json.Required, json.Id> [][]u8,
    b: tag<endian.Big> u16,

   tag<std.fmt.Formatter> fn format(...) ... {
       ....
   }
};

The downside of course is that this adds a keyword, and from what I can tell would have the same issue with inference and comparisons as my other suggestion.

daurnimator commented 5 years ago

@hryx told me I should mention that @field currently works as an lvalue which may obliviate the need for tags. Especially when combined with #2937.

This lets you do things like:

    const ptr = try allocator.alloc(MyConfig);
    inline for (@typeInfo(MyConfig).Struct.StructFields) |f| {
        @field(ptr, f.name) = try conf.get(f.name, f.field_type);
    }

Larger sample here

GoNZooo commented 5 years ago

I think tags make a lot of sense in a system supported by a common set of knowledge and assumptions, but not as a general language feature. It requires more language knowledge, more complicated debugging and more general investigation to figure out what a tag actually means.

Being able to reach tags from arbitrary scopes (even cross-library?) would all but guarantee that you can't prove that you've exhaustively figured out what a tag has made happen.

To put this into a familiar perspective: One of the examples of zig being a clear language is that it doesn't call functions unless it looks like it. With tags, what you get is arbitrary code being executed arbitrarily many times in arbitrary contexts. Something may or may not be caused by a tag and it's generally speaking hard to guarantee you know exactly what.

fengb commented 5 years ago

Tags would only inject additional metadata available at comptime. Any inflection code by its nature is able to do complex dispatch whether tags exist or not. I do agree that it can be confusing when adding new tags could drastically change behavior, but that's already possible with any introspectable value, like alignment or field name.

A usecase where tags would help is serialization code. Go uses tags in JSON to change the outputted field name (e.g. dash case). I'm running into a few issues where Protobuf types almost map correctly to native types, but there are different serialization configurations. Protobuf is a generated type so this isn't a breaking issue, but with tags I could probably make the generated code readable for once.

Edit: without tags, I will probably adopt wrapper structs around primitives. It makes interopt less seamless, but maybe that's a good thing.

fengb commented 4 years ago

More patterns that are currently usable:

const Foo = struct {
    const bar__serialize = "varint";
    bar: u32,

    const baz__serialize = "fixint";
    baz: u32,
};

// or maybe
fn Tag(data: var) type {
    return packed struct {
        pub const tag = data;
    };
};
const Foo = struct {
    const Varint = Tag("varint");
    const Fixint = Tag("fixint");

    bar: u32,
    bar__serialize: Varint = .{},

    baz: u32,
    baz__serialize: Fixint = .{},
};

// semi unified
fn Tagged(comptime T: type, tags_: var) type {
    return packed struct {
        data: T,
        tags: tags_,

        pub const tags = tags_;
    }
}
const Foo = struct {
    bar: Tagged(u32, "varint"),
    baz: Tagged(u32, "fixint"),
}
pgruenbacher commented 4 years ago

Don't forget golang's spec on this. For the most part I think people are happy with them, but they just don't like that it's inspected at runtime I think... but that's where compile-time inspection would be most useful.

type T struct {
    f1     string "f one"
    f2     string
    f3     string `f three`
    f4, f5 int64  `f four and five`
}
func main() {
    t := reflect.TypeOf(T{})
    f1, _ := t.FieldByName("f1")
    fmt.Println(f1.Tag) // f one
    f4, _ := t.FieldByName("f4")
    fmt.Println(f4.Tag) // f four and five
    f5, _ := t.FieldByName("f5")
    fmt.Println(f5.Tag) // f four and five
}
ghost commented 4 years ago

Here's an attempt at getting comptime tags with typedef for zig (#5132). It gets a bit verbose, but it is exactly what comptime tags has been described as: a way to attach data to types that are available at comptime through reflection, with no runtime cost.

Some things that must be considered with typedef for tags:

Example, key-value tag:


const TaggedMyStruct = typedef(MyStruct, .Tag{.keyvalue=.{"key","myvalue"}});

const TagPayloads = struct{
    const tag1 : TypedefConfig.Tag = .Tag{.keyvaluearr=.{.{"key","myvalue"}, .{"otherkey","othervalue"}}});
}
const SecondTaggedMyStruct = typedef(MyStruct, TagPayloads.tag1); // can introduce variable for complex tag payloads

Example, appending and replacing tags through typedef nesting:

const Td1 = typedef(u32, .Tag{.txt=.{"one"}});
const Td2 = typedef(Td1, .Tag{.txt=.{"two"}});
assert(hasTag(Td2,"one") and hasTag(Td2,"two") == true);

_ = typedef(Td2, .Tag{.txt=.{"one"}}); // compile error. cannot append duplicate tag ?

const Td3 = typedef(Td3, .ReTag{.txt=.{"three"}});
assert( (!hasTag(Td3,"one") and hasTag(Td3,"three")) == true);

const Td4 = typedef(u32, .Tag{.keyValue=.{.{"key4","value4"}}});

Tag appends tags, ReTag "clears" all tags below in the typedef hierarchy, and appends the given tag payload. Instead of erasing payloads directly, ReTag would just act as a stop marker, so that a function searching for a tag or key will stop searching if it reaches a ReTag payload.

Example, coercion of typedef tags:

const v = MyTagged_u32 = @as(u32,132);
const x = v + 1; // v coerces down to u32, x becomes u32
// MyDistinct_MyTagged = typedef(MyTagged_u32, .Distinct);
const v2 : MyDistinct_MyTagged_u32 = @as(MyDistinct_MyTagged_u32,132);
const x2 : u32 = v2 + 1; // Note 1: should allow v2 to coerce down 2 steps directly to u32?

fn myfunc(value: MyTagged_u32) bool{ ...} // Note 2: typedef tags should not be valid function argument types

On note 1 in the code comments:

On note 2 in the code comments:

codehz commented 3 years ago

@user00e00 I think they are two different things, this proposal intended to attach metadata into field, your typedef intended to attach metadata into type...

The former is actually a decoration to the parent struct, not intended to decorate the type, so they may not useful to create unique types for every field, it will definitely greatly increase the complexity...

i.e.

const A = struct {
a: u8 @tag("xxx"), // the tag attach to the struct A
b: u8 @tag("yyy"),
}
slimsag commented 3 years ago

It may help to consider the type-safety aspect here, as is being discussed for Go 2: https://github.com/golang/go/issues/23637

More broadly, though, I would argue struct tags and/or more general annotations go against the spirit of:

There is no hidden control flow, no hidden memory allocations, no preprocessor, and no macros.

In Go, struct tags can often cause hidden control flow insofar as the fact that you need to worry about how tags will alter the behavior of your program and it can be difficult to track down what the implication of a struct tag is/isn't:

Tags are clearly useful, but I hope Zig will consider the danger posed by them in the form of tags providing implicit control flow.

Hadron67 commented 3 years ago

A cleaner pattern available in status quo is taking advantage of the anonymous struct literal:

pub const Packet = struct {
    id: u32,
    data: []const u8,

    pub const TAGS = .{
        .id = .{ .var_int = true },
        .data = .{ .max_length = 567 },
    };
};
ThadThompson commented 2 years ago

On the intersection of tags, strings, and comptime generation:

While studying the standard library's JSON parser to learn the 'Zig Way' of serialization, this issue appears in the question of how to interpret a []u8 in the serializer. Currently it looks at the data and takes its best guess:

if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(value)) {
    try outputJsonString(value, options, out_stream);
    return;
}
// Output value as an array

Taking a binary array and examining it at runtime to 'see if it looks like a string' to determine how to send it seems non-deterministically fragile.

Since the resolution of #234 was not to create a distinct datatype for strings, an alternative would be to have a tag on a field indicating precisely how it should be serialized: as a string, as an array, or perhaps a base-64 encoded binary string if one so preferred.

Which, for better or worse is exactly the kind of code generation / control flow changes to which @slimsag is referring. However, this seems to already be happening extensively in places with comptime evaluation like std.fmt and std.json.

16hournaps commented 3 months ago

This will allow creation of requirement based testing and requirement tracing framework directly in the code. No more weird external comment-based tools etc. Useful for any serious quality management to straight up ASIL development. Really like this.

therealcisse commented 3 months ago

Type attributes in zig would be very useful.


const std = @import("std");

const JsonAttr = union(enum) {
    name: struct { []const u8 },
    ignoreNull,

    pub fn name(comptime value: []const u8) JsonAttr.name {
        return JsonAttr{.name = value};
    }

    pub fn ignoreNull() JsonAttr.ignoreNull {
        return JsonAttr{.ignoreNull = {}};
    }

};

const Person attr(.{1, 2, 3}) = struct {
    firstName: []const u8 attr(.{JsonAttr.name("first_name")}),
    middleName: ?[]const u8 attr(.{JsonAttr.name("middle_name"), JsonAttr.ignoreNull()}),
    lastName: []const u8 attr(.{JsonAttr.name("last_name")}),
    age: u32,
};

pub fn main() !void {
    // const person_attrs = std.meta.attrs(Person); // @attrsOf(Person)
    // .{1, 2, 3}

    const fields = std.meta.fields(Person);

    std.debug.print("\n", .{});

    inline for (fields) |f| {
        std.debug.print("name: {s}, type: {s}\n", .{f.name, @typeName(f.type)});

        const attrs: anytype = f.attrs;

        inline for(attrs) |a| {
            switch (a) {
                JsonAttr.name => |n| std.debug.print("{s}\n", .{n}),
                JsonAttr.ignoreNull => std.debug.print("ignoreNull\n", .{}),

            }

            std.debug.print("\n", .{});

        }
    }

    _ = p;
}