Skip to content

Commit 22a5187

Browse files
committed
rename from_allocate to _after_decode and add _before_encode
1 parent 50f1a8d commit 22a5187

3 files changed

Lines changed: 273 additions & 10 deletions

File tree

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
| **Float Precision** | Float16/32/64 with subnormals, Inf, and NaN |
1616
| **Shared References** | Tags 28/29 for deduplication, including cyclic structures |
1717
| **Zero-Copy Decoding** | Both eager and lazy decoding operate directly on the input buffer without copying |
18-
| **Lazy Decoding** | `CBOR::Lazy` for on-demand nested access with result caching |
18+
| **Lazy Decoding** | `CBOR::Lazy` for on-demand nested access with key and result caching |
1919
| **Streaming** | `CBOR.stream` for CBOR sequence reading |
2020
| **Performance** | ~30% faster than msgpack; 1.3–3× faster than simdjson for selective access |
2121

@@ -62,7 +62,7 @@ lazy["missing"] # => KeyError (raises)
6262
lazy.dig("missing", "text") # => nil (safe)
6363
```
6464

65-
**Performance:** Access is O(n) only in skipped elements, not the full document. Results are cached for O(1) repeated access.
65+
**Performance:** Access is O(n) only in skipped elements, not the full document. Keys and Results are cached for O(1) repeated access.
6666
```ruby
6767
# Repeated access uses cache
6868
inner = lazy["outer"]["inner"].value
@@ -116,11 +116,18 @@ class Person
116116
native_ext_type :@name, CBOR::Type::String
117117
native_ext_type :@age, CBOR::Type::Integer
118118

119-
# Called after allocation during decode
120-
def from_allocate
119+
# Called after decoding (optional)
120+
def _after_decode
121121
puts "Person #{@name} loaded"
122122
self
123123
end
124+
125+
# Called before encoding (optional)
126+
# Must return self or a modified object
127+
def _before_encode
128+
@age += 1 if @age < 18 # Example transformation
129+
self
130+
end
124131
end
125132

126133
# Register with a tag number (user-defined: 1000+)
@@ -131,7 +138,7 @@ person.name = "Alice"
131138
person.age = 30
132139

133140
encoded = CBOR.encode(person)
134-
decoded = CBOR.decode(encoded) # => Person object
141+
decoded = CBOR.decode(encoded) # => Person object, after_decode called
135142
```
136143

137144
**Available Types:**

src/mrb_cbor.c

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,6 @@ encode_simple(CborWriter* w, mrb_value obj)
11781178
default: {
11791179
mrb_state *mrb = w->mrb;
11801180
mrb_raise(mrb, E_TYPE_ERROR, "unexpected simple value");
1181-
b = 0;
11821181
} break;
11831182
}
11841183
cbor_writer_write(w, &b, 1);
@@ -1560,6 +1559,10 @@ encode_registered_tag(CborWriter *w, mrb_value obj, mrb_int tag_num)
15601559
{
15611560
mrb_state *mrb = w->mrb;
15621561

1562+
if (mrb_respond_to(mrb, obj, MRB_SYM(_before_encode))) {
1563+
obj = mrb_funcall_argv(mrb, obj, MRB_SYM(_before_encode), 0, NULL);
1564+
}
1565+
15631566
encode_len(w, 6, (uint64_t)tag_num);
15641567

15651568
mrb_value schema = mrb_net_schema(mrb, mrb_class(mrb, obj));
@@ -1572,7 +1575,7 @@ encode_registered_tag(CborWriter *w, mrb_value obj, mrb_int tag_num)
15721575
mrb_hash_foreach(mrb, mrb_hash_ptr(schema),
15731576
encode_registered_tag_foreach, &ctx);
15741577
} else {
1575-
mrb_raise(mrb, E_RUNTIME_ERROR, "registered class has no ned schema");
1578+
mrb_raise(mrb, E_RUNTIME_ERROR, "registered class has no net schema");
15761579
}
15771580
}
15781581

@@ -1671,8 +1674,8 @@ decode_registered_tag(mrb_state *mrb, Reader *r, mrb_value src,
16711674
mrb_hash_foreach(mrb, mrb_hash_ptr(schema),
16721675
decode_registered_tag_foreach, &ctx);
16731676

1674-
if (mrb_respond_to(mrb, obj, MRB_SYM(from_allocate))) {
1675-
return mrb_funcall_argv(mrb, obj, MRB_SYM(from_allocate), 0, NULL);
1677+
if (mrb_respond_to(mrb, obj, MRB_SYM(_after_decode))) {
1678+
return mrb_funcall_argv(mrb, obj, MRB_SYM(_after_decode), 0, NULL);
16761679
} else {
16771680
return obj;
16781681
}

test/test.rb

Lines changed: 254 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,11 @@ def initialize(x, y)
368368
@y = y
369369
end
370370

371-
def from_allocate
371+
def _after_decode
372+
self
373+
end
374+
375+
def _before_encode
372376
self
373377
end
374378

@@ -1566,3 +1570,252 @@ def from_allocate
15661570
# dig on non-container should raise
15671571
assert_raise(TypeError) { lazy.dig("key") }
15681572
end
1573+
1574+
# ============================================================================
1575+
# Registered tag hooks: _before_encode and _after_decode
1576+
# ============================================================================
1577+
1578+
assert('CBOR registered tag: _after_decode hook called') do
1579+
class Person
1580+
attr_accessor :name, :age
1581+
native_ext_type :@name, CBOR::Type::String
1582+
native_ext_type :@age, CBOR::Type::Integer
1583+
1584+
def initialize
1585+
@loaded = false
1586+
end
1587+
1588+
def _after_decode
1589+
@loaded = true
1590+
self
1591+
end
1592+
1593+
def loaded?
1594+
@loaded
1595+
end
1596+
end
1597+
1598+
CBOR.register_tag(1000, Person)
1599+
1600+
person = Person.new
1601+
person.name = "Alice"
1602+
person.age = 30
1603+
1604+
encoded = CBOR.encode(person)
1605+
decoded = CBOR.decode(encoded)
1606+
1607+
assert_true decoded.is_a?(Person)
1608+
assert_equal "Alice", decoded.name
1609+
assert_equal 30, decoded.age
1610+
assert_true decoded.loaded?
1611+
end
1612+
1613+
assert('CBOR registered tag: _before_encode hook called') do
1614+
class Item
1615+
attr_accessor :value
1616+
native_ext_type :@value, CBOR::Type::Integer
1617+
1618+
def initialize(val)
1619+
@value = val
1620+
end
1621+
1622+
def _before_encode
1623+
@value = @value * 2 # Double before encoding
1624+
self
1625+
end
1626+
end
1627+
1628+
CBOR.register_tag(1001, Item)
1629+
1630+
item = Item.new(5)
1631+
encoded = CBOR.encode(item)
1632+
decoded = CBOR.decode(encoded)
1633+
1634+
assert_equal 10, decoded.value
1635+
end
1636+
1637+
assert('CBOR registered tag: both hooks in sequence') do
1638+
class Counter
1639+
attr_accessor :count
1640+
native_ext_type :@count, CBOR::Type::Integer
1641+
1642+
def initialize(n)
1643+
@count = n
1644+
@before_encode_called = false
1645+
@after_decode_called = false
1646+
end
1647+
1648+
def _before_encode
1649+
@before_encode_called = true
1650+
@count = @count + 1
1651+
self
1652+
end
1653+
1654+
def _after_decode
1655+
@after_decode_called = true
1656+
@count = @count + 1
1657+
self
1658+
end
1659+
1660+
def before_encode_called?
1661+
@before_encode_called
1662+
end
1663+
1664+
def after_decode_called?
1665+
@after_decode_called
1666+
end
1667+
end
1668+
1669+
CBOR.register_tag(1002, Counter)
1670+
1671+
counter = Counter.new(10)
1672+
1673+
encoded = CBOR.encode(counter)
1674+
assert_true counter.before_encode_called? # _before_encode was called during encode
1675+
decoded = CBOR.decode(encoded)
1676+
1677+
assert_equal 12, decoded.count # 10 + 1 (before_encode) + 1 (after_decode)
1678+
assert_true decoded.after_decode_called?
1679+
end
1680+
1681+
assert('CBOR registered tag: _before_encode without _after_decode') do
1682+
class Price
1683+
attr_accessor :amount
1684+
native_ext_type :@amount, CBOR::Type::Integer
1685+
1686+
def initialize(val)
1687+
@amount = val
1688+
end
1689+
1690+
def _before_encode
1691+
@amount = (@amount * 100).to_i # Convert to cents
1692+
self
1693+
end
1694+
end
1695+
1696+
CBOR.register_tag(1003, Price)
1697+
1698+
price = Price.new(5)
1699+
encoded = CBOR.encode(price)
1700+
decoded = CBOR.decode(encoded)
1701+
1702+
assert_equal 500, decoded.amount
1703+
assert_true decoded.is_a?(Price)
1704+
end
1705+
1706+
assert('CBOR registered tag: _after_decode without _before_encode') do
1707+
class Config
1708+
attr_accessor :timeout
1709+
native_ext_type :@timeout, CBOR::Type::Integer
1710+
1711+
def initialize(val)
1712+
@timeout = val
1713+
@validated = false
1714+
end
1715+
1716+
def _after_decode
1717+
@timeout = @timeout.abs
1718+
@validated = true
1719+
self
1720+
end
1721+
1722+
def validated?
1723+
@validated
1724+
end
1725+
end
1726+
1727+
CBOR.register_tag(1004, Config)
1728+
1729+
config = Config.new(30)
1730+
encoded = CBOR.encode(config)
1731+
decoded = CBOR.decode(encoded)
1732+
1733+
assert_equal 30, decoded.timeout
1734+
assert_true decoded.validated?
1735+
end
1736+
1737+
assert('CBOR registered tag: hook exception propagates') do
1738+
class Strict
1739+
attr_accessor :value
1740+
native_ext_type :@value, CBOR::Type::Integer
1741+
1742+
def initialize(val)
1743+
@value = val
1744+
end
1745+
1746+
def _before_encode
1747+
raise "encode forbidden"
1748+
end
1749+
end
1750+
1751+
CBOR.register_tag(1005, Strict)
1752+
1753+
strict = Strict.new(42)
1754+
assert_raise(RuntimeError) { CBOR.encode(strict) }
1755+
end
1756+
1757+
assert('CBOR registered tag: hook modifies multiple fields') do
1758+
class User
1759+
attr_accessor :username, :email
1760+
native_ext_type :@username, CBOR::Type::String
1761+
native_ext_type :@email, CBOR::Type::String
1762+
1763+
def initialize
1764+
@username = ""
1765+
@email = ""
1766+
end
1767+
1768+
def _after_decode
1769+
@username = @username.downcase
1770+
@email = @email.downcase
1771+
self
1772+
end
1773+
end
1774+
1775+
CBOR.register_tag(1006, User)
1776+
1777+
user = User.new
1778+
user.username = "AlicE"
1779+
user.email = "Alice@Example.COM"
1780+
1781+
encoded = CBOR.encode(user)
1782+
decoded = CBOR.decode(encoded)
1783+
1784+
assert_equal "alice", decoded.username
1785+
assert_equal "alice@example.com", decoded.email
1786+
end
1787+
1788+
assert('CBOR registered tag: lazy decode with _after_decode') do
1789+
class Data
1790+
attr_accessor :value
1791+
native_ext_type :@value, CBOR::Type::Integer
1792+
1793+
def initialize(val)
1794+
@value = val
1795+
@materialized = false
1796+
end
1797+
1798+
def _after_decode
1799+
@materialized = true
1800+
self
1801+
end
1802+
1803+
def materialized?
1804+
@materialized
1805+
end
1806+
end
1807+
1808+
CBOR.register_tag(1007, Data)
1809+
1810+
data = Data.new(99)
1811+
encoded = CBOR.encode(data)
1812+
lazy = CBOR.decode_lazy(encoded)
1813+
1814+
# Lazy hasn't materialized yet
1815+
assert_false data.materialized?
1816+
1817+
# Calling .value triggers decode and _after_decode
1818+
materialized = lazy.value
1819+
assert_true materialized.materialized?
1820+
assert_equal 99, materialized.value
1821+
end

0 commit comments

Comments
 (0)