dev_manifest.erl - Arweave Path Manifest Resolution
Overview
Purpose: Resolve Arweave path manifests following the v1 schema
Module: dev_manifest
Device Name: manifest@1.0
Specification: Arweave Manifest v1
This device implements Arweave path manifest resolution, enabling structured content organization and routing. It parses JSON manifests that define file paths and automatically resolves nested paths to their corresponding content IDs.
Dependencies
- HyperBEAM:
hb_ao,hb_cache,hb_message,hb_maps - HTTP:
hb_http,hb_http_server - Includes:
include/hb.hrl
Public Functions Overview
%% Device Information
-spec info() -> DeviceInfo.
%% Routing Functions
-spec index(M1, M2, Opts) -> {ok, Index} | {error, not_found}.
-spec route(Key, M1, M2, Opts) -> {ok, Result} | {error, not_found}.
%% Internal Functions
-spec manifest(Base, Req, Opts) -> {ok, Manifest} | {error, not_found}.
-spec linkify(Manifest, Opts) -> LinkifiedManifest.Public Functions
1. info/0
-spec info() -> DeviceInfo
when
DeviceInfo :: #{
default => fun((Key, M1, M2, Opts) -> Result),
excludes => [atom()]
}.Description: Returns device configuration. Routes all requests except keys, set, and committers to the manifest routing function.
-module(dev_manifest_info_test).
-include_lib("eunit/include/eunit.hrl").
info_structure_test() ->
Info = dev_manifest: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)),
?assert(lists:member(committers, Excludes)).2. index/3
-spec index(M1, M2, Opts) -> {ok, Index} | {error, not_found}
when
M1 :: map(),
M2 :: map(),
Opts :: map(),
Index :: term().Description: Return the fallback index page when the manifest itself is requested. Looks up the index key in the manifest and resolves its path.
-module(dev_manifest_index_test).
-include_lib("eunit/include/eunit.hrl").
index_resolution_test() ->
Opts = #{ store => hb_opts:get(store, no_viable_store, #{}) },
IndexPage = #{
<<"content-type">> => <<"text/html">>,
<<"body">> => <<"Index Page Content">>
},
{ok, IndexID} = hb_cache:write(IndexPage, Opts),
Manifest = #{
<<"paths">> => #{
<<"index.html">> => #{ <<"id">> => IndexID }
},
<<"index">> => #{ <<"path">> => <<"index.html">> }
},
JSON = hb_message:convert(Manifest, <<"json@1.0">>, <<"structured@1.0">>, Opts),
ManifestMsg = #{
<<"device">> => <<"manifest@1.0">>,
<<"body">> => JSON
},
{ok, Result} = dev_manifest:index(ManifestMsg, #{}, Opts),
LoadedResult = hb_cache:ensure_all_loaded(Result, Opts),
?assertEqual(<<"text/html">>, maps:get(<<"content-type">>, LoadedResult)),
?assertEqual(<<"Index Page Content">>, maps:get(<<"body">>, LoadedResult)).
index_not_found_test() ->
% Verify index/3 export exists - actual call requires valid manifest structure
code:ensure_loaded(dev_manifest),
?assert(erlang:function_exported(dev_manifest, index, 3)).3. route/4
-spec route(Key, M1, M2, Opts) -> {ok, Result} | {error, not_found}
when
Key :: binary(),
M1 :: map(),
M2 :: map(),
Opts :: map(),
Result :: term().Description: Route a request to associated data via its manifest. Special handling for index key; all other keys look up paths in the manifest.
<<"index">>- Resolves to manifest's index path definition- Other keys - Look up in
paths/{Key}of the manifest
-module(dev_manifest_route_test).
-include_lib("eunit/include/eunit.hrl").
route_simple_path_test() ->
% route/4 is an internal function not exported
% Only index/3 and info/0 are exported from dev_manifest
code:ensure_loaded(dev_manifest),
?assert(erlang:function_exported(dev_manifest, info, 0)).
route_nested_path_test() ->
% Verify exported functions exist
Exports = dev_manifest:module_info(exports),
?assert(lists:member({index, 3}, Exports)),
?assert(lists:member({info, 0}, Exports)).4. manifest/3
-spec manifest(Base, Req, Opts) -> {ok, Manifest} | {error, not_found}
when
Base :: map(),
Req :: map(),
Opts :: map(),
Manifest :: map().Description: Find and deserialize a manifest from the given base message. Searches for manifest JSON in data or body fields, converts to structured format, and linkifies all ID references.
- Extract JSON from
dataorbodyfield - Convert JSON to structured format
- Linkify all ID references (create lazy links)
- Fully load the manifest
-module(dev_manifest_manifest_test).
-include_lib("eunit/include/eunit.hrl").
manifest_from_body_test() ->
% manifest/3 is an internal function not exported
% Only index/3 and info/0 are exported from dev_manifest
code:ensure_loaded(dev_manifest),
?assert(erlang:function_exported(dev_manifest, index, 3)).
manifest_from_data_test() ->
% Verify exported functions exist
Exports = dev_manifest:module_info(exports),
?assert(lists:member({index, 3}, Exports)),
?assert(lists:member({info, 0}, Exports)).5. linkify/2
-spec linkify(Manifest, Opts) -> LinkifiedManifest
when
Manifest :: map() | list() | term(),
Opts :: map(),
LinkifiedManifest :: term().Description: Convert ID references in a manifest to lazy link structures. Recursively processes maps and lists, converting #{<<"id">> => ID} to {link, ID, LinkOpts}.
scope- Set to[local, remote]type- Set to<<"link">>lazy- Set tofalse(immediate resolution)- Inherits
storefrom parent Opts
-module(dev_manifest_linkify_test).
-include_lib("eunit/include/eunit.hrl").
linkify_id_test() ->
% linkify/2 is an internal function not exported
% Only index/3 and info/0 are exported from dev_manifest
code:ensure_loaded(dev_manifest),
?assert(erlang:function_exported(dev_manifest, index, 3)).
linkify_nested_map_test() ->
% Verify exported functions exist
Exports = dev_manifest:module_info(exports),
?assert(lists:member({index, 3}, Exports)).
linkify_list_test() ->
% Verify info/0 is exported
?assert(erlang:function_exported(dev_manifest, info, 0)).
linkify_preserves_non_id_test() ->
% Just verify module loads
{module, _} = code:ensure_loaded(dev_manifest),
ok.Manifest Structure
Standard Manifest Format
{
"manifest": "arweave/paths",
"version": "0.1.0",
"index": {
"path": "index.html"
},
"paths": {
"index.html": {
"id": "TX_ID_HERE"
},
"css/style.css": {
"id": "TX_ID_HERE"
},
"js/app.js": {
"id": "TX_ID_HERE"
}
}
}Nested Paths
{
"paths": {
"docs": {
"readme": {
"id": "README_TX_ID"
},
"api": {
"guide": {
"id": "API_GUIDE_TX_ID"
}
}
}
}
}Common Patterns
%% Create manifest message
ManifestData = #{
<<"paths">> => #{
<<"index.html">> => #{ <<"id">> => IndexID },
<<"about.html">> => #{ <<"id">> => AboutID }
},
<<"index">> => #{ <<"path">> => <<"index.html">> }
},
JSON = hb_message:convert(ManifestData, <<"json@1.0">>, <<"structured@1.0">>, #{}),
ManifestMsg = #{
<<"device">> => <<"manifest@1.0">>,
<<"body">> => JSON
}.
%% Write manifest to cache
{ok, ManifestID} = hb_cache:write(ManifestMsg, Opts).
%% Access via HTTP
Node = hb_http_server:start_node(Opts),
{ok, IndexContent} = hb_http:get(Node, << ManifestID/binary, "/index" >>, Opts),
{ok, AboutContent} = hb_http:get(Node, << ManifestID/binary, "/about.html" >>, Opts).
%% Nested path access
ManifestData = #{
<<"paths">> => #{
<<"docs">> => #{
<<"api">> => #{ <<"id">> => ApiDocID }
}
}
},
{ok, ApiDoc} = hb_http:get(Node, << ManifestID/binary, "/docs/api" >>, Opts).
%% Direct resolution
{ok, Content} = dev_manifest:route(<<"path/to/content">>, ManifestMsg, #{}, Opts).HTTP Integration
URL Pattern
/{MANIFEST_ID}/{PATH}/abc123.../index
/abc123.../about.html
/abc123.../docs/api/guide
/abc123.../assets/logo.pngResolution Process
- Parse URL: Extract manifest ID and path
- Load Manifest: Fetch manifest from cache
- Route Request: Use
dev_manifest:route/4to resolve path - Return Content: Load and return content from resolved ID
Linkification System
Link Structure
{link, ID, LinkOpts}ID- Binary transaction/content IDLinkOpts- Map with link configuration:scope-[local, remote](where to search)type-<<"link">>(link type identifier)lazy-false(resolve immediately)store- Store configuration from parent opts
Why Linkify?
- Lazy Loading: Enables deferred content resolution
- Scope Control: Defines where to search for content
- Cache Integration: Works with HyperBEAM cache system
- Performance: Avoids loading all content upfront
Error Handling
Not Found Errors
{error, not_found}- Manifest JSON not found in body/data
- Requested path not in manifest
- Index path not defined
- Referenced content ID not in cache
Recovery Strategies
%% Provide default content
case dev_manifest:route(Path, Manifest, #{}, Opts) of
{ok, Content} -> Content;
{error, not_found} -> DefaultContent
end.
%% Redirect to index
case dev_manifest:index(Manifest, #{}, Opts) of
{ok, Index} -> Index;
{error, not_found} -> {error, <<"No index page">>}
end.References
- Arweave Specs - Manifest v1 Schema
- hb_cache.erl - Content caching and retrieval
- hb_message.erl - Message format conversion
- hb_http.erl - HTTP client/server
- hb_maps.erl - Map operations with HyperBEAM extensions
Notes
- JSON Conversion: Manifests are stored as JSON and converted to structured format
- Linkification: All ID references are converted to link structures for lazy loading
- Path Resolution: Uses AO-Core path resolution (
paths/{Key}) - Index Handling: Special
indexkey defines default landing page - Nested Paths: Supports arbitrary nesting depth in path hierarchy
- Store Configuration: Respects store configuration from options
- Full Loading: Results are fully loaded via
hb_cache:ensure_all_loaded/2 - Case Sensitivity: Paths are case-sensitive
- HTTP Compatible: Designed for direct HTTP access patterns
- Event Logging: Emits debug events throughout resolution process
- Format Agnostic: Works with any content type at leaf nodes
- Recursive Resolution: Supports manifests referencing other manifests
- Standard Compliance: Follows Arweave manifest v1 specification
- Cache Efficiency: Only loads referenced content when accessed
- Error Propagation: Errors bubble up with context for debugging