Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver lib aerospike key.ex
Raw

lib/aerospike/key.ex

defmodule Aerospike.Key do
@moduledoc """
Record key: namespace, set, optional user key, and server digest.
The digest is **RIPEMD-160** over `set_name || particle_type_byte || encoded_user_key`.
The namespace is **not** part of the digest (standard Aerospike digest rules).
Use `new/3` when you have a user key. Use `from_digest/3` when you already hold the
20-byte digest (no user key material).
Supported user keys for `new/3`:
* `integer()` — encoded as particle type `INTEGER` (1) and 8-byte big-endian signed int64
* `String.t()` (binary) — encoded as particle type `STRING` (3) and UTF-8 bytes
Binaries are treated as **string** keys, not blob keys.
## Examples
iex> k = Aerospike.Key.new("test", "users", "user:42")
iex> byte_size(k.digest)
20
iex> is_binary(k.namespace)
true
"""
import Bitwise
@particle_integer 1
@particle_string 3
@partitions 4096
# Signed 64-bit integer range bounds
@min_int64 -9_223_372_036_854_775_808
@max_int64 9_223_372_036_854_775_807
@enforce_keys [:namespace, :digest]
defstruct namespace: nil, set: "", user_key: nil, digest: nil
@doc """
Returns true if the integer fits in the signed 64-bit range.
## Examples
iex> Aerospike.Key.int64?(0)
true
iex> Aerospike.Key.int64?(9_223_372_036_854_775_807)
true
iex> Aerospike.Key.int64?(9_223_372_036_854_775_808)
false
"""
@spec int64?(integer()) :: boolean()
def int64?(n) when is_integer(n), do: n >= @min_int64 and n <= @max_int64
def int64?(_), do: false
@doc """
Raises ArgumentError if the integer is outside the signed 64-bit range.
"""
@spec validate_int64!(integer(), String.t()) :: :ok
def validate_int64!(n, context \\ "value") when is_integer(n) do
unless int64?(n) do
raise ArgumentError, "#{context} must fit in signed 64-bit range"
end
:ok
end
@type t :: %__MODULE__{
namespace: String.t(),
set: String.t(),
user_key: String.t() | integer() | nil,
digest: <<_::160>>
}
@typedoc "Tuple key form: `{namespace, set, user_key}`."
@type key_tuple :: {String.t(), String.t(), String.t() | integer()}
@typedoc """
Accepted key input at public API boundaries.
Use `%Aerospike.Key{}` directly, or pass `{namespace, set, user_key}` for
convenience when you have user-key components.
"""
@type key_input :: t() | key_tuple()
@doc """
Returns the partition id (0..4095) derived from the digest.
Uses the first four bytes of the digest as a little-endian unsigned 32-bit
integer, then masks to 12 bits (standard partition mapping).
## Examples
iex> k = Aerospike.Key.new("namespace", "set", 0)
iex> Aerospike.Key.partition_id(k)
2451
"""
@spec partition_id(t()) :: 0..4095
def partition_id(%__MODULE__{digest: digest}) when byte_size(digest) == 20 do
<<u::unsigned-little-32, _::binary>> = digest
band(u, @partitions - 1)
end
@doc """
Builds a key and computes the RIPEMD-160 digest.
`namespace` must be a non-empty string. `set` may be an empty string.
`user_key` must be an integer in the int64 range or a binary (string key).
Raises `ArgumentError` if the arguments are invalid.
## Examples
iex> k = Aerospike.Key.new("ns", "s", 1)
iex> byte_size(k.digest)
20
"""
@spec new(String.t(), String.t(), String.t() | integer()) :: t()
def new(namespace, set, user_key)
when is_binary(namespace) and namespace != "" and is_binary(set) and is_integer(user_key) do
validate_int64!(user_key, "user_key")
digest = compute_digest_integer(set, user_key)
%__MODULE__{
namespace: namespace,
set: set,
user_key: user_key,
digest: digest
}
end
def new(namespace, set, user_key)
when is_binary(namespace) and namespace != "" and is_binary(set) and is_binary(user_key) do
digest = compute_digest_string(set, user_key)
%__MODULE__{
namespace: namespace,
set: set,
user_key: user_key,
digest: digest
}
end
def new(_namespace, _set, _user_key) do
raise ArgumentError,
"namespace must be a non-empty string, set must be a string, user_key must be string or int64 integer"
end
@doc """
Builds a key from an existing 20-byte digest (no user key material).
`set` may be an empty string. The digest must already match the server's
record identity for the given namespace and set.
## Examples
iex> d = :crypto.hash(:ripemd160, <<>>)
iex> k = Aerospike.Key.from_digest("ns", "users", d)
iex> k.digest == d
true
iex> k.user_key
nil
"""
@spec from_digest(String.t(), String.t(), <<_::160>>) :: t()
def from_digest(namespace, set, digest)
when is_binary(namespace) and namespace != "" and is_binary(set) and is_binary(digest) and
byte_size(digest) == 20 do
%__MODULE__{
namespace: namespace,
set: set,
user_key: nil,
digest: digest
}
end
def from_digest(_namespace, _set, _digest) do
raise ArgumentError,
"namespace must be a non-empty string, set must be a string, digest must be a 20-byte binary"
end
@doc """
Coerces a public key input into `%Aerospike.Key{}`.
Passes `%Aerospike.Key{}` through unchanged. For tuple keys, delegates to
`new/3`, so tuple validation and int64 checks follow the same rules.
Raises `ArgumentError` for non-key inputs.
"""
@spec coerce!(key_input()) :: t()
def coerce!(%__MODULE__{} = key), do: key
def coerce!({namespace, set, user_key}),
do: new(namespace, set, user_key)
def coerce!(_key) do
raise ArgumentError,
"expected %Aerospike.Key{} or {namespace, set, user_key} tuple where user_key is a string or int64 integer"
end
@doc """
Formats a key as `namespace:set:user_key:digest_hex`.
If `user_key` is unavailable (keys built with `from_digest/3`), the
user-key segment is rendered as `_`.
"""
@spec to_string(t()) :: String.t()
def to_string(%__MODULE__{} = key) do
digest_hex = Base.encode16(key.digest, case: :lower)
"#{key.namespace}:#{key.set}:#{user_key_segment(key.user_key)}:#{digest_hex}"
end
defp compute_digest_integer(set, user_key) when is_integer(user_key) do
data = <<set::binary, @particle_integer::8, user_key::64-signed-big>>
:crypto.hash(:ripemd160, data)
end
defp compute_digest_string(set, user_key) when is_binary(user_key) do
data = <<set::binary, @particle_string::8, user_key::binary>>
:crypto.hash(:ripemd160, data)
end
defp user_key_segment(nil), do: "_"
defp user_key_segment(user_key) when is_binary(user_key), do: user_key
defp user_key_segment(user_key) when is_integer(user_key), do: Integer.to_string(user_key)
end
defimpl String.Chars, for: Aerospike.Key do
def to_string(%Aerospike.Key{} = key), do: Aerospike.Key.to_string(key)
end
defimpl Inspect, for: Aerospike.Key do
import Inspect.Algebra
def inspect(%Aerospike.Key{} = key, _opts) do
concat(["#Aerospike.Key<", Aerospike.Key.to_string(key), ">"])
end
end