I’m trying to create an enum SupportedOS such that it’s a subset of std.Target.Os.Tag.
I need this because the library I’m writing needs to have behavior that’s dependent on target OS. The enum subset helps me write test exhaustively without referencing the original Os.Tag enum which covers a lot more operating systems than the library can support atm.
Wondering if I’m over-engineering here or perhaps there’s a language feature I missed.
pub const SupportedOS = enum {
windows,
linux,
freebsd,
macos,
other,
};
comptime {
for (std.enums.values(SupportedOS)) |f| {
if (!@hasField(std.Target.Os.Tag, @tagName(f)))
@compileError("SupportedOS contains field not present in Os.Tag: " ++ @tagName(f));
}
}
/// Returns `SupportedOS` enumeration based on provided `tag`. If the tag
/// signifies an unsupported operating system, this method will return `other`.
pub fn getSupportedOS(tag: std.Target.Os.Tag) SupportedOS {
inline for (std.enums.values(SupportedOS)) |f| {
if (std.meta.eql(@field(std.Target.Os.Tag, @tagName(f)), tag))
return @field(SupportedOS, @tagName(f));
}
return .other;
}
I love your solution, it is elegant. But as you already said it is overengineered. It is the proper solution if you expect to have switch statements on getSupportedOS and you want to handle all OS cases. If you expect to write if (getSupportedOS(builtin.os.tag) == .windows) { then a better solution might be to use helper functions like:
Another option would be to define or generate the entries in SupportedOS to have the same value like their entry in std.Target.Os.Tag, then your getSupportedOS function would just become this:
Basically, make a fn isSupported(os: std.Target.Os.Tag) bool then build a custom SupportedOs type at comptime, along with helpers to convert back-and-forth between the two types.
It seems a bit nicer to me, though its hard to say why. For one, your ‘SupportedOS contains field not present in Os.Tag’ error message wouldn’t be necessary anymore, because it’d be impossible to get into that situation. I also like his use of null instead of .other.
If you expect to write if (getSupportedOS(builtin.os.tag) == .windows) { then a better solution might be to use helper functions like:
The problem with this is that you can’t switch on isWindows. You also probably want to make this function inline and return type comptime_int to make sure it doesn’t evaluate runtime. Or always remember to call it in a comptime context.
Another option would be to define or generate the entries in SupportedOS to have the same value like their entry in std.Target.Os.Tag, then your getSupportedOS function would just become this
You can also avoid the ABI compatiblity with original enum, by doing return std.meta.stringToEnum(SupportedOS, @tagName(tag)) orelse .other; this is basically same as the original getSupportedOS function.
I don’t think I would describe it as ABI compatibility, while it uses the value of the enum, it is only an implementation detail (so it is more an Application Binary Implementation Detail, then it is an Interface, because the user still only uses the name with some implementation specific value) and if you generate it / rely on the super enum’s values it is automatically in sync (so I don’t think there is a big difference between matching names only or values too).
I think the code of stringToEnum is a bit more complex instead of matching simple values to eachother, in general I think matching enum values in useful ways is a bit underrated.
That said, both approaches have their benefits depending on use case.
Thinking a bit more about it, I would change the subset to still have the entries with matching values but then use a non-exhaustive enum for the subset, that way you can get rid of .other and instead use _ for other, if you define the subset enum with the same backing int you can just do @enumFromInt(@intFromEnum(tag)), additionally you can even do the same to convert it back to the super-set, so the whole thing just becomes some integer with 2 two different, but related sets of names.
Wow. The resulting API is really pretty and extensible. It’s even more over-engineering though. I might just have to bite the bullet. I can’t risk adding support for a platform but then forgetting to implement a feature.
I think Sze has it right. On launch, I’m just supported the os’s that represent the largest marketshare. The end goal of the library is to support all the platform that zig itself supports (which is a tall order).
I expect the library to grow and all features should work on all supported operating systems. Finally, adding a supported operating system should be easy to do (ideally a one line change – and compiler jumps into action telling me what I’ve missed).
I like it. I think you could refine it even further to a comptime function that takes your base enum type and a list of enum literals to create the same thing with less boilerplate.