Skip to content

Commit

Permalink
Remove the IpAddresses type
Browse files Browse the repository at this point in the history
Using multiple IP addresses in a particular order is really only
relevant for TcpClient.new and TcpClient.with_timeout. Binding a socket
is pretty much always done with a single IP address, and when multiple
IPs _are_ used it's likely that you want to bind to _all_ of them, not
the first one that works. For the low-level Socket type we don't do
anything special either, as it's up to consumers of that type to decide
how they want to connect when handling multiple IPs.
  • Loading branch information
yorickpeterse committed Jan 1, 2025
1 parent 10358df commit 888832e
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 258 deletions.
12 changes: 6 additions & 6 deletions std/src/std/net/dns.inko
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import std.cmp (Equal)
import std.fmt (Format, Formatter)
import std.io
import std.net.ip (IpAddresses)
import std.net.ip (IpAddress)
import std.string (ToString)
import std.sys.linux.dns (self as sys) if linux
import std.sys.unix.dns (self as sys) if mac
Expand Down Expand Up @@ -71,9 +71,9 @@ trait Resolve {

# Resolves the given hostname into a list of IP addresses.
#
# Upon success an `std.net.ip.IpAddresses` is returned, storing the IP
# addresses in the order in which they should be used based on the underlying
# platform's preferences.
# Upon success an array of `IpAddress` values is returned, storing them in the
# order in which they should be used based on the underlying platform's
# preferences.
#
# # Errors
#
Expand Down Expand Up @@ -101,7 +101,7 @@ trait Resolve {
# .try(fn (ip) { TcpClient.new(ip, port: 80) })
# .or_panic('failed to connect')
# ```
fn pub mut resolve(host: String) -> Result[IpAddresses, Error]
fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error]
}

# A type for performing DNS queries.
Expand Down Expand Up @@ -188,7 +188,7 @@ impl Resolve for Resolver {
@inner.reset_deadline
}

fn pub mut resolve(host: String) -> Result[IpAddresses, Error] {
fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] {
@inner.resolve(host)
}
}
144 changes: 0 additions & 144 deletions std/src/std/net/ip.inko
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import std.clone (Clone)
import std.cmp (Equal)
import std.fmt (Format as FormatTrait, Formatter)
import std.int (Format)
import std.iter (Stream)
import std.string (IntoString, StringBuffer, ToString)

# The byte for a single dot (".").
Expand Down Expand Up @@ -943,146 +942,3 @@ impl Clone[Ipv4Address] for Ipv4Address {
Ipv4Address(a: @a, b: @b, c: @c, d: @d)
}
}

# An ordered collection of IP addresses.
#
# Certain operations such as DNS lookups may yield multiple IP addresses. In
# such a case it's typically expected to process those addresses in the order
# they are returned in. This type allows doing exactly that using the method
# `IpAddresses.try`, without the need for duplicating the logic in different
# places.
type pub inline IpAddresses {
let @values: Array[IpAddress]

# Returns a new `IpAddresses` that takes ownership of the given array of IP
# addresses.
#
# # Examples
#
# ```inko
# import std.net.ip (IpAddress, IpAddresses)
#
# IpAddresses.new([])
# IpAddresses.new([IpAddress.v4(127, 0, 0, 1)])
# ```
fn pub static new(ips: Array[IpAddress]) -> IpAddresses {
IpAddresses(ips)
}

# Returns an iterator over the IP addresses stored in `self`.
#
# # Examples
#
# ```inko
# import std.net.ip (IpAddress, IpAddresses)
#
# let ips = IpAddresses.new([IpAddress.v4(127, 0, 0, 1)])
#
# ips.iter.each(fn (ip) {
# ip # => IpAddress.v4(127, 0, 0, 1)
# })
# ```
fn pub iter -> Stream[IpAddress] {
@values.iter
}

# Calls `fun` for each IP address in `self`, returning the first
# `Result.Ok(R)` it returns.
#
# If none of the IPs produce a `Result.Ok`, the error returned by the last
# call of `fun` is returned.
#
# This method allows performing of an operation using multiple IP addresses,
# returning a value for the first IP address that produces a successful
# result. For example, makes it possible to establish a socket connection
# using multiple IP addresses in the same order as the IP addresses.
#
# # Panics
#
# This method panics if `self` is empty, as there's no meaningful fallback
# error to produce in such a case.
#
# # Examples
#
# ```inko
# import std.net.ip (IpAddress, IpAddresses)
# import std.net.socket (TcpServer)
#
# IpAddresses
# .new([IpAddress.v4(127, 0, 0, 1), IpAddress.v4(127, 0, 0, 100)])
# .try(fn (ip) { TcpServer.new(ip, port: 9999) })
# .or_panic('failed to set up the server')
# ```
fn pub try[R, E](fun: fn (IpAddress) -> Result[R, E]) -> Result[R, E] {
let iter = @values.iter
let mut last = Option.None

loop {
match iter.next {
case Some(ip) -> {
match fun.call(ip) {
case Ok(v) -> return Result.Ok(v)
case Error(e) -> last = Option.Some(e)
}
}
case _ -> break
}
}

Result.Error(last.or_panic('at least one IP address must be specified'))
}

# Returns the underlying array of IP addresses, consuming `self` in the
# process.
#
# # Examples
#
# ```inko
# import std.net.ip (IpAddress, IpAddresses)
#
# IpAddresses.new([IpAddress.v4(127, 0, 0, 1)]).into_array # => [IpAddress.v4(127, 0, 0, 1)]
# ```
fn pub move into_array -> Array[IpAddress] {
@values
}

# Returns the number of values in `self`.
#
# # Examples
#
# ```inko
# import std.net.ip (IpAddress, IpAddresses)
#
# IpAddresses.new([]).size # => 0
# IpAddresses.new([IpAddress.v4(127, 0, 0, 1)]).size # => 1
# ```
fn pub size -> Int {
@values.size
}

# Returns `true` if `self` is empty.
#
# # Examples
#
# ```inko
# import std.net.ip (IpAddress, IpAddresses)
#
# IpAddresses.new([]).empty? # => true
# IpAddresses.new([IpAddress.v4(127, 0, 0, 1)]).empty? # => false
# ```
fn pub empty? -> Bool {
@values.empty?
}
}

impl FormatTrait for IpAddresses {
fn pub fmt(formatter: mut Formatter) {
@values.fmt(formatter)
}
}

impl Equal[ref IpAddresses] for IpAddresses {
fn pub ==(other: ref IpAddresses) -> Bool {
@values == other.values
}
}
74 changes: 59 additions & 15 deletions std/src/std/net/socket.inko
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ import std.time (Duration, ToInstant)
# Linux (for example) would have no effect.
let MAXIMUM_LISTEN_BACKLOG = 65_535

fn pub try_ips[R, E](
ips: ref Array[IpAddress],
fun: fn (IpAddress) -> Result[R, E],
) -> Result[R, E] {
let iter = ips.iter
let mut last = Option.None

loop {
match iter.next {
case Some(ip) -> {
match fun.call(ip) {
case Ok(v) -> return Result.Ok(v)
case Error(e) -> last = Option.Some(e)
}
}
case _ -> break
}
}

Result.Error(last.or_panic('at least one IP address must be specified'))
}

trait RawSocketOperations {
fn mut raw_socket -> Pointer[net.RawSocket]

Expand Down Expand Up @@ -789,13 +811,20 @@ type pub TcpClient {
Result.Ok(TcpClient(socket))
}

# Creates a new `TcpClient` that is connected to the TCP socket at the given
# IP address and port.
# Creates a new `TcpClient` that's connected to an IP address and port number.
#
# If multiple IP addresses are given, this method attempts to connect to them
# in order, returning upon the first successful connection. If no connection
# can be established, the error of the last attempt is returned.
#
# This method doesn't enforce a deadline on establishing the connection. If
# you need to limit the amount of time spent waiting to establish the
# connection, use `TcpClient.with_timeout` instead.
#
# # Panics
#
# This method panics if `ips` is empty.
#
# # Examples
#
# Connecting a `TcpClient`:
Expand All @@ -804,23 +833,36 @@ type pub TcpClient {
# import std.net.socket (TcpClient)
# import std.net.ip (IpAddress)
#
# TcpClient.new(IpAddress.v4(127, 0, 0, 1), port: 40_000).get
# TcpClient.new([IpAddress.v4(127, 0, 0, 1)], port: 40_000).get
# ```
fn pub static new(ip: ref IpAddress, port: Int) -> Result[TcpClient, Error] {
let socket = try Socket.stream(ip.v6?)
fn pub static new(
ips: ref Array[IpAddress],
port: Int,
) -> Result[TcpClient, Error] {
try_ips(ips, fn (ip) {
let socket = try Socket.stream(ip.v6?)

try socket.connect(ip, port)
from(socket)
try socket.connect(ip, port)
from(socket)
})
}

# Creates a new `TcpClient` but limits the amount of time spent waiting for
# the connection to be established.
#
# If multiple IP addresses are given, this method attempts to connect to them
# in order, returning upon the first successful connection. If no connection
# can be established, the error of the last attempt is returned.
#
# The `timeout_after` argument specifies the deadline after which the
# `connect()` times out. The deadline is cleared once connected.
#
# See `TcpClient.new` for more information.
#
# # Panics
#
# This method panics if `ips` is empty.
#
# # Examples
#
# ```inko
Expand All @@ -830,23 +872,25 @@ type pub TcpClient {
#
# TcpClient
# .with_timeout(
# ip: IpAddress.v4(0, 0, 0, 0),
# ips: [IpAddress.v4(0, 0, 0, 0)],
# port: 40_000,
# timeout_after: Duration.from_secs(5)
# )
# .get
# ```
fn pub static with_timeout[T: ToInstant](
ip: ref IpAddress,
ips: ref Array[IpAddress],
port: Int,
timeout_after: ref T,
) -> Result[TcpClient, Error] {
let socket = try Socket.stream(ip.v6?)

socket.timeout_after = timeout_after
try socket.connect(ip, port)
socket.reset_deadline
from(socket)
try_ips(ips, fn (ip) {
let socket = try Socket.stream(ip.v6?)

socket.timeout_after = timeout_after
try socket.connect(ip, port)
socket.reset_deadline
from(socket)
})
}

# Returns the local address of this socket.
Expand Down
8 changes: 4 additions & 4 deletions std/src/std/sys/linux/dns.inko
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import std.io
import std.json (Json)
import std.libc
import std.net.dns (Error, Resolve)
import std.net.ip (IpAddresses)
import std.net.ip (IpAddress)
import std.net.socket (UnixClient)
import std.sys.unix.dns (resolver as system_resolver)
import std.sys.unix.net (self as net)
Expand All @@ -27,7 +27,7 @@ fn resolve_host(host: String) -> String {
}

# Parses the response of the `ResolveHostname` call.
fn parse_resolve_host_response(json: Json) -> Result[IpAddresses, Error] {
fn parse_resolve_host_response(json: Json) -> Result[Array[IpAddress], Error] {
let params = json.query.key('parameters')

if json.query.key('error').as_string.some? {
Expand Down Expand Up @@ -73,7 +73,7 @@ fn parse_resolve_host_response(json: Json) -> Result[IpAddresses, Error] {
},
)

Result.Ok(IpAddresses(ips))
Result.Ok(ips)
}

# A resolver that uses systemd-resolve's through its
Expand All @@ -99,7 +99,7 @@ impl Resolve for SystemdResolver {
@socket.socket.reset_deadline
}

fn pub mut resolve(host: String) -> Result[IpAddresses, Error] {
fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] {
@buffer.append(resolve_host(host))
@buffer.push(0)
try @socket.write_bytes(@buffer).map_error(fn (e) { Error.Other(e) })
Expand Down
6 changes: 3 additions & 3 deletions std/src/std/sys/unix/dns.inko
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import std.libc.freebsd (self as sys_libc) if freebsd
import std.libc.linux (self as sys_libc) if linux
import std.libc.mac (self as sys_libc) if mac
import std.net.dns (Error, Resolve)
import std.net.ip (IpAddresses)
import std.net.ip (IpAddress)
import std.sys.unix.net (self as sys_net) if unix
import std.time (ToInstant)

Expand All @@ -24,7 +24,7 @@ impl Resolve for Resolver {
# getaddrinfo() doesn't support timeouts.
}

fn pub mut resolve(host: String) -> Result[IpAddresses, Error] {
fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] {
let hints = sys_libc.AddrInfo(
ai_flags: (libc.AI_ADDRCONFIG | libc.AI_V4MAPPED) as Int32,
ai_family: libc.AF_UNSPEC as Int32,
Expand Down Expand Up @@ -64,7 +64,7 @@ impl Resolve for Resolver {
}

libc.freeaddrinfo(list)
Result.Ok(IpAddresses(ips))
Result.Ok(ips)
}
}

Expand Down
Loading

0 comments on commit 888832e

Please sign in to comment.