-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor!: make the API harder to misuse #261
Conversation
c652d4a
to
f43a278
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great. ❤️
Left some comments; I don't feel strongly about them.
// validateComponent MUST be called after creating a non-zero Component. | ||
// It ensures that we will be able to call all methods on Component without | ||
// error. | ||
func validateComponent(c Component) (Component, error) { | ||
_, err := c.valueAndErr() | ||
if err != nil { | ||
return Component{}, err | ||
|
||
} | ||
if c.protocol.Transcoder != nil { | ||
err = c.protocol.Transcoder.ValidateBytes([]byte(c.bytes[c.offset:])) | ||
if err != nil { | ||
return Component{}, err | ||
} | ||
} | ||
return c, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT
I'd move this logic inside newComponent
and ensure that all components are created by newComponent
.
Currently there are 4 places in code where we need to do validateComponent(component{...})
and possibly all of them can be changed to newComponent(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only see two usages of validateComponent
:
- In
newComponent
. - In
readComponent
(it happens twice in that method)
These two functions are a bit different. newComponent
is expecting a creating a Component
from parts, while readComponent
is creating a Component from a byte array. Changing these methods to be the same is a bit awkward as Component.bytes
is the full bytes representation of the component, so we would introduce a copy somewhere if we did so.
I think it's fine to leave as is.
addr, err := ma.NewMultiaddr("/ip4/1.2.3.4/tcp/80") | ||
// err non-nil when parsing failed. | ||
*/ | ||
type Multiaddr interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
func (m Multiaddr) EncapsulateC(c Component) Multiaddr { | ||
if c.Empty() { | ||
return m | ||
} | ||
out := make([]Component, 0, len(m)+1) | ||
out = append(out, m...) | ||
out = append(out, c) | ||
return out | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the multiaddr is immutable, can we just appent to the existing slice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the argument to remove this method or change this to append?
The caller is always free to use append directly. This method does a copy on purpose to avoid sharing the underlying slice. If the caller is okay with that they can append
directly.
This seeks to refactor the codebase to make it much harder to hit nil pointer dereference panics.
This takes a different approach to how we've treated multiaddrs in the past. Instead of attempting to make them a general and performant datastructure, we focus on treating them as just an encoding scheme. Users of multiaddrs are expected to parse the multiaddr into some struct that is suitable for their use case, and use the multiaddr form when interoperating. By treating Multiaddrs as just an encoding scheme we can make a number of simplifications in the codebase. Specifically this PR does the following:
Multiaddr
interface.[]Component
Multiaddr
interface as there is none.Background
This library has had multiple issues related to
Multiaddr
being an interface. Many methods use and returnnil
as the zero value, which behaves poorly when the user forgets to do a nil check on every returned value and attempts to call a method on the nil pointer. For example, usingSplit
to split a Multiaddr and then usingJoin
to rebuild the original Multiaddr historically would panic in case one side of the split was nil. Using an interface also leads to incorrect usages of==
to check if two Multiaddrs were equal (would only work for pointer equality) and incorrectly usingMultiaddr
as a key for a map.Using an interface is typically done to provide a consistent API surface for multiple implementing types. In practice however, the Multiaddr interface was only implemented for
multiaddr
andcomponent
(with arguably some awkwardness when using a component as a Multiaddr).The better approach is to use a concrete type for a Multiaddr. This lets pointer receiver methods work even if the pointer is nil, since the compiler already knows which function to call. Most methods now take a value rather than a pointer which avoids the issue of a nil pointer dereference completely.
Migration
Refer to ./v015-MIGRATION.md for breaking changes and migration tips