dev_patch.erl - Message Reorganization & Patching Device
Overview
Purpose: Move and reorganize data between message paths
Module: dev_patch
Pattern: Source Path → Target Path Data Movement
Version: Patch@1.0
This device reorganizes messages by moving data from one path to another within message structures. It operates in two modes: moving all data from a source path, or selectively moving PATCH-tagged submessages. Supports both execution-device standard hooks and standalone operation.
Operation Modes
1. All Mode
Moves all data from source path to target path.
2. Patches Mode (Default)
Moves only submessages with:
methodkey ofPATCHdevicekey ofpatch@1.0
Dependencies
- HyperBEAM:
hb_ao,hb_path,hb_maps,hb_message - Arweave: None direct
- Includes:
include/hb.hrl - Standards: Implements
execution-deviceinterface
Public Functions Overview
%% Primary Functions
-spec all(Msg1, Msg2, Opts) -> {ok, PatchedMsg} | {error, Reason}.
-spec patches(Msg1, Msg2, Opts) -> {ok, PatchedMsg} | {error, Reason}.
%% Execution Device Standard Hooks
-spec init(Msg1, Msg2, Opts) -> {ok, Msg1}.
-spec compute(Msg1, Msg2, Opts) -> {ok, PatchedMsg}.
-spec normalize(Msg1, Msg2, Opts) -> {ok, Msg1}.
-spec snapshot(Msg1, Msg2, Opts) -> {ok, Msg1}.Path Resolution
Search Order (for from and to keys)
patch-Xkey in execution message (Msg2)Xkey in execution message (Msg2)patch-Xkey in request message (Msg1)Xkey in request message (Msg1)
Where X is either from or to.
Path Prefixes
% Paths can be prefixed to specify source message
<<"base:/path">> % Relative to Msg1 (base/execution message)
<<"req:/path">> % Relative to Msg2 (request message)
<<"/path">> % Defaults to Msg1 (base message)Public Functions
1. all/3
-spec all(Msg1, Msg2, Opts) -> {ok, PatchedMsg} | {error, not_found}
when
Msg1 :: map(),
Msg2 :: map(),
Opts :: map(),
PatchedMsg :: map().Description: Move all data from source path to target path, regardless of content or structure.
Test Code:-module(dev_patch_all_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
all_mode_basic_test() ->
InitState = #{
<<"device">> => <<"patch@1.0">>,
<<"input">> => #{
<<"zones">> => #{
<<"1">> => #{
<<"prices">> => #{
<<"apple">> => 100,
<<"banana">> => 200
}
},
<<"2">> => #{
<<"prices">> => #{
<<"orange">> => 300
}
}
}
},
<<"state">> => #{
<<"prices">> => #{
<<"apple">> => 1000
}
}
},
{ok, ResolvedState} = hb_ao:resolve(
InitState,
#{
<<"path">> => <<"all">>,
<<"patch-to">> => <<"/state">>,
<<"patch-from">> => <<"/input/zones">>
},
#{}
),
?assertEqual(100, hb_ao:get(<<"state/1/prices/apple">>, ResolvedState, #{})),
?assertEqual(300, hb_ao:get(<<"state/2/prices/orange">>, ResolvedState, #{})),
?assertEqual(not_found, hb_ao:get(<<"input/zones">>, ResolvedState, #{})).
all_with_base_prefix_test() ->
BaseMsg = #{
<<"device">> => <<"patch@1.0">>,
<<"data">> => #{
<<"original">> => <<"value1">>
}
},
ReqMsg = #{
<<"path">> => <<"all">>,
<<"patch-from">> => <<"base:/data">>,
<<"patch-to">> => <<"/result">>
},
{ok, Result} = hb_ao:resolve(BaseMsg, ReqMsg, #{}),
?assertEqual(<<"value1">>, hb_ao:get(<<"result/original">>, Result, #{})),
?assertEqual(not_found, hb_ao:get(<<"data">>, Result, #{})).2. patches/3
-spec patches(Msg1, Msg2, Opts) -> {ok, PatchedMsg} | {error, not_found}
when
Msg1 :: map(),
Msg2 :: map(),
Opts :: map(),
PatchedMsg :: map().Description: Selectively move only submessages marked with method=PATCH or device=patch@1.0 from source to target path. Non-PATCH messages remain at source.
- Extracts messages where
method == <<"PATCH">> - Extracts messages where
device == <<"patch@1.0">> - Removes
commitmentsandTagsfields from patches - Removes
methodfield after moving - Non-matching messages stay in source
-module(dev_patch_patches_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
patches_mode_basic_test() ->
InitState = #{
<<"device">> => <<"patch@1.0">>,
<<"results">> => #{
<<"outbox">> => #{
<<"1">> => #{
<<"method">> => <<"PATCH">>,
<<"prices">> => #{
<<"apple">> => 100,
<<"banana">> => 200
}
},
<<"2">> => #{
<<"method">> => <<"GET">>,
<<"prices">> => #{
<<"apple">> => 1000
}
}
}
},
<<"patch-to">> => <<"/">>,
<<"patch-from">> => <<"/results/outbox">>
},
{ok, ResolvedState} = hb_ao:resolve(
InitState,
<<"compute">>,
#{}
),
?assertEqual(100, hb_ao:get(<<"prices/apple">>, ResolvedState, #{})),
?assertEqual(not_found, hb_ao:get(<<"results/outbox/1">>, ResolvedState, #{})),
% Non-PATCH message remains in source
?assertMatch(
#{<<"method">> := <<"GET">>},
hb_ao:get(<<"results/outbox/2">>, ResolvedState, #{})
).
patches_to_submessage_test() ->
InitState = #{
<<"device">> => <<"patch@1.0">>,
<<"results">> => #{
<<"outbox">> => #{
<<"1">> => hb_message:commit(#{
<<"method">> => <<"PATCH">>,
<<"prices">> => #{
<<"apple">> => 100,
<<"banana">> => 200
}
}, hb:wallet())
}
},
<<"state">> => #{
<<"prices">> => #{
<<"apple">> => 1000
}
},
<<"patch-to">> => <<"/state">>,
<<"patch-from">> => <<"/results/outbox">>
},
{ok, ResolvedState} = hb_ao:resolve(
InitState,
<<"compute">>,
#{}
),
?assertEqual(100, hb_ao:get(<<"state/prices/apple">>, ResolvedState, #{})).
patches_with_device_tag_test() ->
InitState = #{
<<"device">> => <<"patch@1.0">>,
<<"results">> => #{
<<"outbox">> => #{
<<"1">> => #{
<<"device">> => <<"patch@1.0">>,
<<"data">> => <<"patched-value">>
}
}
},
<<"patch-to">> => <<"/">>,
<<"patch-from">> => <<"/results/outbox">>
},
{ok, ResolvedState} = hb_ao:resolve(
InitState,
<<"compute">>,
#{}
),
?assertEqual(<<"patched-value">>, hb_ao:get(<<"data">>, ResolvedState, #{})).3. compute/3 (Execution Device Hook)
-spec compute(Msg1, Msg2, Opts) -> {ok, PatchedMsg}
when
Msg1 :: map(),
Msg2 :: map(),
Opts :: map(),
PatchedMsg :: map().Description: Standard execution-device compute hook. Delegates to patches/3 for default behavior.
-module(dev_patch_compute_test).
-include_lib("eunit/include/eunit.hrl").
compute_hook_test() ->
% compute/3 requires proper path format that hb_path can handle
% Verify module exports compute/3
code:ensure_loaded(dev_patch),
?assert(erlang:function_exported(dev_patch, compute, 3)).4. init/3, normalize/3, snapshot/3
-spec init(Msg1, Msg2, Opts) -> {ok, Msg1}.
-spec normalize(Msg1, Msg2, Opts) -> {ok, Msg1}.
-spec snapshot(Msg1, Msg2, Opts) -> {ok, Msg1}.Description: Standard execution-device hooks required for compliance. All pass through Msg1 unchanged.
Test Code:-module(dev_patch_hooks_test).
-include_lib("eunit/include/eunit.hrl").
hooks_passthrough_test() ->
Msg = #{ <<"data">> => <<"value">> },
?assertEqual({ok, Msg}, dev_patch:init(Msg, #{}, #{})),
?assertEqual({ok, Msg}, dev_patch:normalize(Msg, #{}, #{})),
?assertEqual({ok, Msg}, dev_patch:snapshot(Msg, #{}, #{})).Path Resolution Examples
%% Absolute path from base message (default)
#{
<<"patch-from">> => <<"/results/outbox">>,
<<"patch-to">> => <<"/state">>
}
%% Explicit base message reference
#{
<<"patch-from">> => <<"base:/results/outbox">>,
<<"patch-to">> => <<"/state">>
}
%% Request message reference
#{
<<"patch-from">> => <<"req:/input/data">>,
<<"patch-to">> => <<"/state">>
}
%% Root path
#{
<<"from">> => <<"/">>,
<<"to">> => <<"/backup">>
}Common Patterns
%% Apply PATCH messages from outbox to root state
State = #{
<<"device">> => <<"patch@1.0">>,
<<"results">> => #{
<<"outbox">> => #{
<<"1">> => #{ <<"method">> => <<"PATCH">>, <<"x">> => 1 },
<<"2">> => #{ <<"method">> => <<"PATCH">>, <<"y">> => 2 }
}
},
<<"patch-from">> => <<"/results/outbox">>,
<<"patch-to">> => <<"/">>
},
{ok, Patched} = hb_ao:resolve(State, <<"compute">>, #{}).
% Result: #{<<"x">> => 1, <<"y">> => 2, ...}
%% Move all data (not just patches)
State = #{
<<"device">> => <<"patch@1.0">>,
<<"temp">> => #{ <<"data">> => <<"value">> }
},
Request = #{
<<"path">> => <<"all">>,
<<"from">> => <<"/temp">>,
<<"to">> => <<"/permanent">>
},
{ok, Moved} = hb_ao:resolve(State, Request, #{}).
%% Use in execution stack
Process = #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"stack@1.0">>,
<<"execution-stack">> => [
<<"lua@5.3a">>,
<<"patch@1.0">>,
<<"multipass@1.0">>
],
<<"patch-from">> => <<"/results/outbox">>,
<<"patch-to">> => <<"/">>
}.
%% Patch with custom set device (e.g., trie)
State = #{
<<"device">> => <<"patch@1.0">>,
<<"results">> => #{
<<"outbox">> => #{
<<"1">> => #{
<<"device">> => <<"patch@1.0">>,
<<"balances">> => #{
<<"device">> => <<"trie@1.0">>
}
},
<<"2">> => #{
<<"device">> => <<"patch@1.0">>,
<<"balances">> => #{
<<"A">> => <<"50">>,
<<"B">> => <<"100">>
}
}
}
},
<<"patch-from">> => <<"/results/outbox">>
},
{ok, Result} = hb_ao:resolve(State, <<"compute">>, #{}).
% Custom devices properly handled during patchingIntegration with AO Processes
%% AOS process with state patching
Process = #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"stack@1.0">>,
<<"execution-stack">> => [
<<"wasi@1.0">>,
<<"json-iface@1.0">>,
<<"wasm-64@1.0">>,
<<"patch@1.0">>, % Apply patches from Lua
<<"multipass@1.0">>
]
},
%% Lua code that generates PATCH messages
LuaCode = <<"
table.insert(ao.outbox.Messages, {
method = 'PATCH',
x = 'banana',
y = 42
})
">>,
%% After execution, patch@1.0 moves PATCH messages to process state
{ok, Result} = hb_ao:resolve(Process, #{
<<"target">> => ProcessID,
<<"action">> => <<"Eval">>,
<<"data">> => LuaCode
}, #{}).
% State updated with x="banana", y=42Behavior Details
Source Path Processing
- Parse
frompath with prefix handling - Resolve path against appropriate message (base or req)
- Return error if source not found
Target Path Application
- Parse
topath (always relative to base message) - Apply filtered/all data to target
- Remove source data from original location
PATCH Message Filtering
- Strips
commitmentsfield - Strips
Tagsfield - Removes
methodfield after moving - Preserves all other data
New Source Value
allmode: Sets source tounset(removes it)patchesmode: Keeps non-PATCH messages at source
Error Handling
%% Source path not found
{error, not_found}
%% Source path exists but is empty
{ok, MessageWithEmptyTarget}
%% Invalid path syntax
% Caught by hb_path moduleUse Cases
-
State Updates from Computation Results
- Move PATCH messages from outbox to process state
- Common in AO process execution stacks
-
Data Reorganization
- Move temporary data to permanent locations
- Restructure message hierarchies
-
Selective Message Processing
- Filter specific message types
- Apply only relevant updates
-
Cross-Message Data Transfer
- Move data from request to base message
- Consolidate information from multiple sources
-
Custom Device Integration
- Works with devices having custom
setimplementations - Preserves device-specific behavior (e.g., trie@1.0)
- Works with devices having custom
References
- Execution Device Standard - Device interface compliance
- Stack Device -
dev_stack.erlfor execution pipelines - AO Process -
dev_process.erlfor process execution - Path Handling -
hb_path.erlfor path parsing - Map Operations -
hb_maps.erlfor deep map manipulation
Notes
- Default Mode:
compute/3usespatchesmode, notall - Method Removal: PATCH method removed after applying
- Commitment Stripping: Signatures/commitments removed from patches
- Source Deletion: Source path cleared after successful move
- Path Defaults: Both
fromandtodefault to/if not specified - Prefix Parsing: Only
fromsupports base:/req: prefixes - Execution Stack: Often used as final step in execution pipeline
- Custom Devices: Respects custom
setimplementations - Error Return: Uses
maybesyntax, returns{error, not_found}on failure - Map Keys: Works with both string and binary keys in maps