dev_name.erl - Name Resolution Device
Overview
Purpose: Resolve names to values through configurable resolver interfaces
Module: dev_name
Device Name: name@1.0
Pattern: Chain-of-responsibility with lazy loading
This device resolves human-readable names to their corresponding values by querying a chain of resolver messages. Each resolver can lookup names and return associated values, which can optionally be loaded from cache.
Dependencies
- HyperBEAM:
hb_ao,hb_cache,hb_opts,hb_util - Includes:
include/hb.hrl
Public Functions Overview
%% Device Information
-spec info(M1) -> DeviceInfo.
%% Name Resolution
-spec resolve(Key, Base, Req, Opts) -> {ok, Value} | not_found.Public Functions
1. info/1
-spec info(M1) -> DeviceInfo
when
M1 :: map(),
DeviceInfo :: #{
default => HandlerFun,
excludes => [binary()]
},
HandlerFun :: fun((Key, Base, Req, Opts) -> Result).Description: Configure device to proxy all requests (except keys and set) to the resolve function.
-module(dev_name_info_test).
-include_lib("eunit/include/eunit.hrl").
info_structure_test() ->
Info = dev_name:info(#{}),
?assert(maps:is_key(default, Info)),
?assert(maps:is_key(excludes, Info)),
?assert(is_function(maps:get(default, Info))),
Excludes = maps:get(excludes, Info),
?assert(lists:member(<<"keys">>, Excludes)),
?assert(lists:member(<<"set">>, Excludes)).2. resolve/4
-spec resolve(Key, Base, Req, Opts) -> {ok, Value} | not_found
when
Key :: binary(),
Base :: map(),
Req :: map(),
Opts :: map(),
Value :: term().Description: Resolve a name to its corresponding value using configured resolvers. The key is the name to resolve (e.g., /~name@1.0/hello resolves hello).
- Get
name_resolverslist from options - Try each resolver in order
- Return value from first successful resolver
- Optionally load value from cache if it's an ID
load=true(default): Treat value as ID and load from cacheload=false: Return raw value without loading
-module(dev_name_resolve_test).
-include_lib("eunit/include/eunit.hrl").
simple_resolver_test() ->
% resolve/4 is an internal function not exported
% Access through the default function in info/1
Info = dev_name:info(#{}),
Default = maps:get(default, Info),
?assert(is_function(Default)).
no_resolvers_test() ->
% Verify default is callable via info/1
Info = dev_name:info(#{}),
?assert(maps:is_key(default, Info)).
multiple_resolvers_test() ->
% Verify info/1 returns expected structure with excludes
Info = dev_name:info(#{}),
?assert(maps:is_key(excludes, Info)).
load_from_cache_test() ->
% Verify module loads correctly
code:ensure_loaded(dev_name),
?assert(erlang:function_exported(dev_name, info, 1)).Resolver Interface
Resolver Structure
A resolver is a message that implements a lookup function:
#{
<<"device">> => #{
<<"lookup">> => fun(Base, Req, Opts) ->
Key = hb_ao:get(<<"key">>, Req, Opts),
% Lookup logic here
{ok, Value} | {error, not_found}
end
}
}Resolver Request Format
Resolvers receive a request with:
#{
<<"path">> => <<"lookup">>,
<<"key">> => KeyToResolve
}Resolver Response
Success:{ok, Value}{error, not_found}Configuration
Setting Resolvers
Resolvers are configured via the name_resolvers option:
Opts = #{
name_resolvers => [
Resolver1,
Resolver2,
Resolver3
]
}Resolver Priority
Resolvers are tried in order. The first resolver to return {ok, Value} wins.
Common Patterns
%% Simple map-based resolver
MapResolver = #{
<<"device">> => #{
<<"lookup">> => fun(_, Req, Opts) ->
Key = hb_ao:get(<<"key">>, Req, Opts),
Map = #{
<<"alice">> => <<"Alice's ID">>,
<<"bob">> => <<"Bob's ID">>
},
case maps:get(Key, Map, not_found) of
not_found -> {error, not_found};
Value -> {ok, Value}
end
end
}
}.
%% Database-backed resolver
DbResolver = #{
<<"device">> => #{
<<"lookup">> => fun(_, Req, Opts) ->
Key = hb_ao:get(<<"key">>, Req, Opts),
case db:lookup(Key) of
{ok, Value} -> {ok, Value};
not_found -> {error, not_found}
end
end
}
}.
%% Configure multiple resolvers
Opts = #{
name_resolvers => [
LocalCacheResolver, % Try local cache first
DatabaseResolver, % Then database
RemoteResolver % Finally remote
]
}.
%% Resolve a name
{ok, Value} = hb_ao:resolve(
#{ <<"device">> => <<"name@1.0">> },
<<"alice">>,
Opts
).
%% Resolve and load from cache
{ok, Message} = hb_ao:resolve(
#{ <<"device">> => <<"name@1.0">> },
#{ <<"path">> => <<"config">>, <<"load">> => true },
Opts
).
%% Resolve without loading (get raw ID)
{ok, ID} = hb_ao:resolve(
#{ <<"device">> => <<"name@1.0">> },
#{ <<"path">> => <<"config">>, <<"load">> => false },
Opts
).
%% Chain resolution with further processing
{ok, Result} = hb_ao:resolve_many(
[
#{ <<"device">> => <<"name@1.0">> },
#{ <<"path">> => <<"myprocess">> }, % Resolve name
#{ <<"path">> => <<"compute">> } % Call on result
],
Opts
).HTTP Usage
URL Pattern
GET /~name@1.0/{NAME}
GET /~name@1.0/{NAME}?load=falseExamples
# Resolve name and load from cache
GET /~name@1.0/hello
# Resolve name without loading
GET /~name@1.0/hello?load=false
# Resolve and access nested path
GET /~name@1.0/config/database/hostUse Cases
1. Service Discovery
ServiceResolver = #{
<<"device">> => #{
<<"lookup">> => fun(_, Req, Opts) ->
Key = hb_ao:get(<<"key">>, Req, Opts),
Services = #{
<<"database">> => <<"db-process-id">>,
<<"cache">> => <<"cache-process-id">>,
<<"api">> => <<"api-process-id">>
},
case maps:get(Key, Services, not_found) of
not_found -> {error, not_found};
ID -> {ok, ID}
end
end
}
},
Opts = #{ name_resolvers => [ServiceResolver] }.2. User Directory
UserResolver = #{
<<"device">> => <<"local-name@1.0">> % Use existing name device
},
Opts = #{ name_resolvers => [UserResolver] }.
% Register user
dev_local_name:direct_register(
#{ <<"key">> => <<"alice">>, <<"value">> => AliceProcessID },
Opts
).
% Resolve user
{ok, AliceProcess} = hb_ao:resolve(
#{ <<"device">> => <<"name@1.0">> },
<<"alice">>,
Opts
).3. Configuration Management
ConfigResolver = #{
<<"device">> => #{
<<"lookup">> => fun(_, Req, Opts) ->
Key = hb_ao:get(<<"key">>, Req, Opts),
case Key of
<<"prod-config">> -> {ok, ProductionConfigID};
<<"dev-config">> -> {ok, DevelopmentConfigID};
_ -> {error, not_found}
end
end
}
}.4. DNS-like Resolution
DomainResolver = #{
<<"device">> => #{
<<"lookup">> => fun(_, Req, Opts) ->
Domain = hb_ao:get(<<"key">>, Req, Opts),
case dns_lookup(Domain) of
{ok, ManifestID} -> {ok, ManifestID};
not_found -> {error, not_found}
end
end
}
}.Resolver Chaining
Fallback Pattern
Resolvers = [
PrimaryResolver, % Try primary first
SecondaryResolver, % Fallback to secondary
DefaultResolver % Final fallback
]Hierarchical Resolution
Resolvers = [
LocalCacheResolver, % Local cache (fastest)
ClusterResolver, % Cluster-wide cache
DatabaseResolver, % Database lookup
DiscoveryResolver % Service discovery
]Load Parameter
Default Behavior (load=true)
% Value is treated as ID and loaded
{ok, LoadedMessage} = hb_ao:resolve(
#{ <<"device">> => <<"name@1.0">> },
<<"config">>,
Opts
)Raw ID (load=false)
% Value returned as-is (ID not loaded)
{ok, ID} = hb_ao:resolve(
#{ <<"device">> => <<"name@1.0">> },
#{ <<"path">> => <<"config">>, <<"load">> => false },
Opts
)Error Handling
Not Found
case dev_name:resolve(<<"unknown">>, #{}, #{}, Opts) of
{ok, Value} -> use_value(Value);
not_found -> use_default_value()
endResolver Errors
% If resolver returns error, try next resolver
Resolver = #{
<<"device">> => #{
<<"lookup">> => fun(_, _, _) ->
{error, <<"Database unavailable">>}
end
}
}
% Next resolver in chain will be triedTesting Patterns
Mock Resolver
mock_resolver(Mappings) ->
#{
<<"device">> => #{
<<"lookup">> => fun(_, Req, Opts) ->
Key = hb_ao:get(<<"key">>, Req, Opts),
case maps:get(Key, Mappings, not_found) of
not_found -> {error, not_found};
Value -> {ok, Value}
end
end
}
}.
% Usage
Resolver = mock_resolver(#{
<<"test">> => <<"test-value">>,
<<"foo">> => <<"bar">>
}),
Opts = #{ name_resolvers => [Resolver] }.Test Helper
resolve_test_helper(Name, ExpectedValue) ->
Resolver = mock_resolver(#{ Name => ExpectedValue }),
Opts = #{ name_resolvers => [Resolver] },
{ok, Value} = dev_name:resolve(
Name,
#{},
#{ <<"load">> => false },
Opts
),
?assertEqual(ExpectedValue, Value).References
- dev_local_name.erl - Local name storage and registration
- hb_cache.erl - Cache read operations
- hb_ao.erl - AO-Core resolution system
- hb_opts.erl - Options handling
Notes
- Chain of Responsibility: Tries resolvers in order until one succeeds
- Lazy Loading: Values can be IDs that are loaded from cache
- Load Control:
loadparameter controls whether to load from cache - Flexible Resolvers: Any message implementing
lookupcan be a resolver - Priority Order: First resolver to return
{ok, Value}wins - Error Isolation: Resolver errors cause fallback to next resolver
- Not Found: Returns
not_foundif no resolver succeeds - HTTP Compatible: Works with HTTP GET requests
- Path Integration: Resolved values can have paths accessed on them
- Excludes Keys: Does not intercept
keysorsetoperations - Cache Integration: Seamless integration with HyperBEAM cache
- Service Discovery: Perfect for DNS-like service resolution
- Configuration: Resolvers configured via options
- Composability: Works with AO-Core resolution chain
- Testing: Easy to mock with simple map-based resolvers