Skip to content

Commit 1e40d02

Browse files
committed
rework native_ext_type
1 parent 8a15c41 commit 1e40d02

3 files changed

Lines changed: 386 additions & 1051 deletions

File tree

README.md

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ Exceeding this raises `RuntimeError: "CBOR nesting depth exceeded"`. Override by
4646

4747
- **Encoding:** ~30% faster than msgpack (SBO + incremental writes)
4848
- **Lazy decoding:** 1.3–3× faster than simdjson for selective access
49-
- **Shared refs:** Tags 28/29 deduplication is O(1) amortized
5049
- **Float encoding:** Width is fixed at compile time; no runtime overhead
5150

5251
**When to use lazy decoding:**
@@ -143,52 +142,69 @@ result.equal?(result[0]) # => true (self-referential)
143142

144143
### Custom Tags & Type Registration
145144

146-
Register your own classes for CBOR encoding/decoding.
145+
Register your own classes for CBOR encoding/decoding. The `native_ext_type` DSL declares which ivars to encode/decode and their expected Ruby type. Any Ruby class works as a type constraint — including other registered classes, enabling nested structures.
146+
147147
```ruby
148-
class Person
149-
attr_accessor :name, :age
148+
class Address
149+
native_ext_type :@street, String
150+
native_ext_type :@city, String
151+
end
150152

151-
# Declare which fields to encode/decode and their expected CBOR types
152-
native_ext_type :@name, CBOR::Type::String
153-
native_ext_type :@age, CBOR::Type::Integer
153+
CBOR.register_tag(1001, Address)
154+
155+
class Person
156+
native_ext_type :@name, String
157+
native_ext_type :@age, Integer
158+
native_ext_type :@address, Address # nested registered class
154159

155160
# Called after decoding (optional)
156161
def _after_decode
157162
puts "Person #{@name} loaded"
158163
self
159164
end
160165

161-
# Called before encoding (optional)
162-
# Must return self or a modified object
166+
# Called before encoding (optional). Must return self or a modified object.
163167
def _before_encode
164168
@age += 1 if @age < 18
165169
self
166170
end
167171
end
168172

169-
# Register with a tag number (user-defined: 1000+)
170173
CBOR.register_tag(1000, Person)
171174

175+
addr = Address.new
176+
addr.instance_variable_set(:@street, "Main St")
177+
addr.instance_variable_set(:@city, "Berlin")
178+
172179
person = Person.new
173-
person.name = "Alice"
174-
person.age = 30
180+
person.instance_variable_set(:@name, "Alice")
181+
person.instance_variable_set(:@age, 30)
182+
person.instance_variable_set(:@address, addr)
175183

176184
encoded = CBOR.encode(person)
177185
decoded = CBOR.decode(encoded) # => Person object, _after_decode called
186+
187+
decoded.instance_variable_get(:@name) # => "Alice"
188+
decoded.instance_variable_get(:@address).class # => Address
189+
decoded.instance_variable_get(:@address).instance_variable_get(:@city) # => "Berlin"
178190
```
179191

180-
**Available Types:**
181-
- `CBOR::Type::UnsignedInt`
182-
- `CBOR::Type::NegativeInt`
183-
- `CBOR::Type::String` (UTF-8 text)
184-
- `CBOR::Type::Bytes` (raw bytes)
185-
- `CBOR::Type::Array`, `CBOR::Type::Map`
186-
- `CBOR::Type::Tagged` (for Bigint, your own registered Tags)
187-
- `CBOR::Type::Simple` (nil, false, true, Floats)
192+
**Type constraints use standard Ruby classes:**
193+
194+
| Schema type | Accepts |
195+
|-------------|---------|
196+
| `String` | UTF-8 strings and byte strings |
197+
| `Integer` | Integer values (fixnum or bigint) |
198+
| `Float` | Floating-point values |
199+
| `Array` | Arrays |
200+
| `Hash` | Maps |
201+
| `TrueClass` / `FalseClass` | Booleans |
202+
| `NilClass` | nil |
203+
| Any registered class | Instances of that class (or subclasses) |
204+
205+
Type checking uses `is_a?`, so inheritance works: a schema of `Numeric` accepts both `Integer` and `Float`, and a schema of `Animal` accepts any subclass of `Animal`.
188206

189-
Convenience Types:
190-
- `CBOR::Type::BytesOrString`
191-
- `CBOR::Type::Integer`
207+
Fields absent from a decoded payload are silently skipped — only declared ivars are populated (allowlist model). Extra fields in the payload are ignored.
192208

193209
---
194210

@@ -280,8 +296,6 @@ end
280296
Use this when you control the read loop yourself, e.g. in an event-driven or async context:
281297

282298
```ruby
283-
decoder = CBOR.stream(sock) { |doc| handle(doc.value) }
284-
# or equivalently:
285299
decoder = CBOR::StreamDecoder.new { |doc| handle(doc.value) }
286300

287301
# Feed chunks as they arrive
@@ -296,7 +310,7 @@ end
296310

297311
| Source type | Detection | Behaviour |
298312
|-------------|-----------|-----------|
299-
| String-like | reponds to `bytesize` and `byteslice` | Walks buffer directly, no copy |
313+
| String-like | responds to `bytesize` and `byteslice` | Walks buffer directly, no copy |
300314
| Socket-like | responds to `recv` | Accumulates chunks, yields complete docs |
301315
| File-like | responds to `seek` and `read` | Uses `seek`+`read` with doubling read-ahead |
302316

@@ -325,7 +339,7 @@ rake test
325339
| `RangeError` | Integer out of bounds | Encoding a Bigint larger than uint64 |
326340
| `RuntimeError` | Nesting depth exceeded | Deeply nested structures beyond `CBOR_MAX_DEPTH` |
327341
| `RuntimeError` | Truncated/invalid CBOR | `CBOR.decode(incomplete_buffer)` |
328-
| `TypeError` | Type mismatch in registered tags | Field marked as String gets an Array |
342+
| `TypeError` | Type mismatch in registered tags | Field declared as `String` receives an `Array` |
329343
| `TypeError` | Unknown stream source | `CBOR.stream(42)` |
330344
| `KeyError` | Lazy access to missing key | `lazy["nonexistent"]` (use `.dig` to get nil instead) |
331345
| `NotImplementedError` | Presym on non-presym mruby | `CBOR.symbols_as_uint32` on build without presym |

0 commit comments

Comments
 (0)