inetaf / netaddr

Network address types

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Why do IPPrefixFrom and IPRangeFrom permit constructing invalid values?

danderson opened this issue · comments

IPPrefix and IPRange have a Valid method. This was very necessary when the types were transparent, since you could put whatever you wanted in them.

Now that the types are opaque, should their constructors return errors? Then we can guarantee that a particular IPPrefix/IPRange is always valid or zero, rather than have to deal with the possibility of invalid inputs into other functions.

If yes, should we also add Must* helpers that panic on error?

I'm intrigued by the idea of making invalid values unrepresentable, but also worried that it makes the API unwieldy to manipulate. thoughts?

You can still make a zero value that's invalid, even with an opaque type, e.g. https://play.golang.org/p/kAegzb_KbyG ....

func main() {
	v := reflect.Value{}
	fmt.Println(v.IsValid()) // false
}

The Must idiom is Go is generally reserved for things that will be made in init as package globals (regexp.MustCompile, template.Must). I'm wary of adding ones that are tempting to use in normal program control flow and panic at runtime rather than at init time. These might be okay, but IPSet probably isn't (as you can't use IPSet in a single expression anyway, so if you're doing it at init, it's via a func)

Speaking of reflect, though, that's the other style we could imitate: let you make invalid values, give you an IsValid accessor, but panic on misuse. The reflect package panics like crazy on misuse (I see 86 references to "panic" in its godoc). Arguably it's a low-level weirdo package, and maybe not best to imitate, but it still might be the best option, rather than polluting constructor call sites with error checks (that are usually fine to do without) and without slightly unidiomatic Must prefixes everywhere.

I'd much rather have a sleek, easy-to-use API with IsValid and documented panics than an awkward and/or bloated API.

I'd also rather have an API than panics than an API that silently changes my inputs to be valid (or drops invalid inputs), although I feel less strongly about that.

I'm wary of documented panics, because that still leads to runtime failures. However, my main interest is in getting consistency between all our types, and I don't really like any of the available options. So, if y'all like one of the options, let's do that, and I'll replace my in-flight PR with one that implements whatever we decide.

However, my main interest is in getting consistency between all our types, and I don't really like any of the available options.

My feelings exactly.

Thinking on it a bit more, I think I actually prefer the "silently fix things up" style of API: if you pass a zoned IP to a place where zones make no sense, it gets stripped out. If you pass a zero IP into something, it does nothing (which makes sense, you handed me nothing, I do nothing). I think our docs can do a good job of saying "look, if you tell us to do something nonsensical, and there's something reasonable we can do instead, we'll do that." Especially because a lot of the edge cases concern zones and invalid objects, which should be unusual in the first place. Let's optimize our design for people who used one of the errorful Parse* helpers to construct valid objects, and who aren't doing strange things with zones.

if you pass a zoned IP to a place where zones make no sense, it gets stripped out.

The thing that makes me nervous about this is if you are using an IPPrefix to (in your mind) hold an IP and a number of bits. You might reasonably expect that ipp.IP() will give you back what you passed in originally, even if in the context of a prefix it didn't make any sense. The result is a subtle and probably rare/latent bug, rather than a highly visible, easy to diagnose early failure.

As I said, though, I don't feel strongly about this.

Maybe going back to specific, concrete API questions will help.

In the particular case of IPPrefix, I'm most inclined to accept that they're a bit too big, and have an easy to understand, easy to implement, easy to use API.

There's no such easy out for IPSetBuilder, unfortunately. One option might be to change IPSetBuilder.IPSet to return an IPSet and an error. Then we can return whatever IPSet the user has constructed, and if they ever added something invalid when constructing the IPSet, we can also return an error telling them that. Then the current behavior is equivalent to ignoring the error returned from IPSetBuilder.IPSet. And most of the IPSetBuilder API remains lightweight.

Discussed during our design session. We cannot reasonably prevent the construction of invalid values due to zero values, so we're keeping the current behavior of having Valid functions.