Class: Takagi::CBOR::Encoder

Inherits:
Object
  • Object
show all
Defined in:
lib/takagi/cbor/encoder.rb,
sig/takagi/cbor/encoder.rbs

Overview

CBOR Encoder (RFC 8949)

Encodes Ruby objects to CBOR binary format. Optimized for IoT/CoAP workloads with minimal footprint.

Supported types:

  • Integers (signed/unsigned, up to 64-bit)
  • Floats (64-bit IEEE 754)
  • Strings (UTF-8)
  • Byte strings (binary data)
  • Arrays
  • Hashes (maps)
  • Booleans (true/false)
  • nil (null)
  • Time (timestamp, tag 1)

Examples:

Basic encoding

Encoder.encode({ temperature: 25.5, humidity: 60 })
# => "\xA2ktempera..." (CBOR bytes)

Encoding with symbols

Encoder.encode({ temp: 25.5 })
# Symbols converted to strings

Constant Summary collapse

MAJOR_TYPE_UNSIGNED_INT =

CBOR Major Types (RFC 8949 §3)

Returns:

  • (0)
0
MAJOR_TYPE_NEGATIVE_INT =

Returns:

  • (1)
1
MAJOR_TYPE_BYTE_STRING =

Returns:

  • (2)
2
MAJOR_TYPE_TEXT_STRING =

Returns:

  • (3)
3
MAJOR_TYPE_ARRAY =

Returns:

  • (4)
4
MAJOR_TYPE_MAP =

Returns:

  • (5)
5
MAJOR_TYPE_TAG =

Returns:

  • (6)
6
MAJOR_TYPE_SIMPLE =

Returns:

  • (7)
7
SIMPLE_FALSE =

Simple values (RFC 8949 §3.3)

Returns:

  • (20)
20
SIMPLE_TRUE =

Returns:

  • (21)
21
SIMPLE_NULL =

Returns:

  • (22)
22
SIMPLE_FLOAT64 =

Returns:

  • (27)
27
TAG_EPOCH_TIMESTAMP =

Tag values (RFC 8949 §3.4)

Returns:

  • (1)
1
MAX_UINT8 =

Maximum safe integer values

Returns:

  • (255)
0xFF
MAX_UINT16 =

Returns:

  • (65535)
0xFFFF
MAX_UINT32 =

Returns:

  • (4294967295)
0xFFFFFFFF
MAX_UINT64 =

Returns:

  • (18446744073709551615)
0xFFFFFFFFFFFFFFFF
TYPE_HANDLERS =

Returns:

  • (::Hash[untyped, :encode_integer | :encode_float | :encode_string | :encode_array | :encode_map | :encode_simple | :encode_timestamp])
{
  Integer => :encode_integer,
  Float => :encode_float,
  String => :encode_string,
  Array => :encode_array,
  Hash => :encode_map,
  TrueClass => :encode_simple,
  FalseClass => :encode_simple,
  NilClass => :encode_simple,
  Time => :encode_timestamp
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.encode(obj) ⇒ String

Encode a Ruby object to CBOR bytes

Examples:

Encoder.encode(42)              # => "\x18\x2A"
Encoder.encode("hello")         # => "ehello"
Encoder.encode([1, 2, 3])       # => "\x83\x01\x02\x03"
Encoder.encode({ a: 1 })        # => "\xA1aa\x01"

Parameters:

  • obj (Object)

    Ruby object to encode

Returns:

  • (String)

    CBOR-encoded binary string

Raises:



78
79
80
# File 'lib/takagi/cbor/encoder.rb', line 78

def encode(obj)
  new.encode(obj)
end

Instance Method Details

#encode(obj) ⇒ String

Encode a Ruby object to CBOR bytes

Parameters:

  • obj (Object)

    Ruby object to encode

Returns:

  • (String)

    CBOR-encoded binary string

Raises:



88
89
90
# File 'lib/takagi/cbor/encoder.rb', line 88

def encode(obj)
  encode_value(obj)
end

#encode_array(arr) ⇒ Object

Encode array (major type 4) RFC 8949 §3.1

Parameters:

  • arr (Object)

Returns:

  • (Object)


176
177
178
179
180
181
182
183
184
# File 'lib/takagi/cbor/encoder.rb', line 176

def encode_array(arr)
  result = encode_with_length(MAJOR_TYPE_ARRAY, arr.size)

  arr.each do |item|
    result << encode_value(item)
  end

  result
end

#encode_byte_string(bytes) ⇒ Object

Encode byte string (major type 2) RFC 8949 §3.1

Parameters:

  • bytes (Object)

Returns:

  • (Object)


168
169
170
171
172
# File 'lib/takagi/cbor/encoder.rb', line 168

def encode_byte_string(bytes)
  byte_length = bytes.bytesize

  encode_with_length(MAJOR_TYPE_BYTE_STRING, byte_length) + bytes
end

#encode_float(float) ⇒ Object

Encode float (major type 7, additional info 27) RFC 8949 §3.3: Always use 64-bit IEEE 754

Parameters:

  • float (Object)

Returns:

  • (Object)


148
149
150
151
152
153
154
# File 'lib/takagi/cbor/encoder.rb', line 148

def encode_float(float)
  # Major type 7, additional info 27 (64-bit float)
  major_byte = (MAJOR_TYPE_SIMPLE << 5) | SIMPLE_FLOAT64

  # Pack as big-endian 64-bit float (network byte order)
  [major_byte].pack('C') + [float].pack('G')
end

#encode_integer(int) ⇒ Object

Encode integer (major type 0 or 1)

Parameters:

  • int (Object)

Returns:

  • (Object)


118
119
120
121
122
123
124
# File 'lib/takagi/cbor/encoder.rb', line 118

def encode_integer(int)
  if int >= 0
    encode_unsigned_int(int)
  else
    encode_negative_int(int)
  end
end

#encode_map(hash) ⇒ Object

Encode map/hash (major type 5) RFC 8949 §3.1

Parameters:

  • hash (Object)

Returns:

  • (Object)


188
189
190
191
192
193
194
195
196
197
# File 'lib/takagi/cbor/encoder.rb', line 188

def encode_map(hash)
  result = encode_with_length(MAJOR_TYPE_MAP, hash.size)

  hash.each do |key, value|
    result << encode_value(key)
    result << encode_value(value)
  end

  result
end

#encode_negative_int(int) ⇒ Object

Encode negative integer (major type 1) RFC 8949 §3.1: -1 - n

Parameters:

  • int (Object)

Returns:

  • (Object)


136
137
138
139
140
141
142
143
144
# File 'lib/takagi/cbor/encoder.rb', line 136

def encode_negative_int(int)
  # Convert to CBOR representation: -1 - n
  # Example: -1 => 0, -2 => 1, -500 => 499
  n = -1 - int

  raise EncodeError, "Integer too small: #{int}" if n > MAX_UINT64

  encode_with_length(MAJOR_TYPE_NEGATIVE_INT, n)
end

#encode_simple(obj) ⇒ Object

Encode simple values (major type 7) RFC 8949 §3.3

Parameters:

  • obj (Object)

Returns:

  • (Object)


201
202
203
204
205
206
207
208
209
210
# File 'lib/takagi/cbor/encoder.rb', line 201

def encode_simple(obj)
  simple_value = case obj
                 when false then SIMPLE_FALSE
                 when true then SIMPLE_TRUE
                 when nil then SIMPLE_NULL
                 end

  major_byte = (MAJOR_TYPE_SIMPLE << 5) | simple_value
  [major_byte].pack('C')
end

#encode_string(str) ⇒ Object

Encode UTF-8 string (major type 3) RFC 8949 §3.1

Parameters:

  • str (Object)

Returns:

  • (Object)


158
159
160
161
162
163
164
# File 'lib/takagi/cbor/encoder.rb', line 158

def encode_string(str)
  # Ensure UTF-8 encoding
  utf8_str = str.encode('UTF-8')
  byte_length = utf8_str.bytesize

  encode_with_length(MAJOR_TYPE_TEXT_STRING, byte_length) + utf8_str
end

#encode_symbol(symbol) ⇒ Object

Parameters:

  • symbol (Object)

Returns:

  • (Object)


113
114
115
# File 'lib/takagi/cbor/encoder.rb', line 113

def encode_symbol(symbol)
  encode_string(symbol.to_s)
end

#encode_timestamp(time) ⇒ Object

Encode timestamp (tag 1, epoch seconds) RFC 8949 §3.4.2

Parameters:

  • time (Object)

Returns:

  • (Object)


214
215
216
217
218
219
220
221
222
# File 'lib/takagi/cbor/encoder.rb', line 214

def encode_timestamp(time)
  # Tag 1: Epoch-based timestamp (integer seconds since 1970-01-01)
  tag_byte = encode_with_length(MAJOR_TYPE_TAG, TAG_EPOCH_TIMESTAMP)

  # Encode timestamp as integer (seconds since epoch)
  timestamp_int = time.to_i

  tag_byte + encode_value(timestamp_int)
end

#encode_unsigned_int(int) ⇒ Object

Encode unsigned integer (major type 0) RFC 8949 §3.1

Parameters:

  • int (Object)

Returns:

  • (Object)


128
129
130
131
132
# File 'lib/takagi/cbor/encoder.rb', line 128

def encode_unsigned_int(int)
  raise EncodeError, "Integer too large: #{int}" if int > MAX_UINT64

  encode_with_length(MAJOR_TYPE_UNSIGNED_INT, int)
end

#encode_value(obj) ⇒ Object

Parameters:

  • obj (Object)

Returns:

  • (Object)


94
95
96
97
98
99
100
101
# File 'lib/takagi/cbor/encoder.rb', line 94

def encode_value(obj)
  handler = handler_for(obj)
  send(handler, obj)
rescue EncodeError
  raise
rescue StandardError => e
  raise EncodeError, "Encoding failed: #{e.message}"
end

#encode_with_length(major_type, length) ⇒ Object

Encode major type with length/value RFC 8949 §3: Additional Information encoding

Additional info: 0-23: Value directly in additional info 24: 1-byte uint8 follows 25: 2-byte uint16 follows 26: 4-byte uint32 follows 27: 8-byte uint64 follows

Parameters:

  • major_type (Object)
  • length (Object)

Returns:

  • (Object)


233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/takagi/cbor/encoder.rb', line 233

def encode_with_length(major_type, length)
  if length < 24
    # Value fits in additional info (0-23)
    major_byte = (major_type << 5) | length
    [major_byte].pack('C')
  elsif length <= MAX_UINT8
    # 1-byte length follows (24-255)
    major_byte = (major_type << 5) | 24
    [major_byte, length].pack('CC')
  elsif length <= MAX_UINT16
    # 2-byte length follows (256-65535)
    major_byte = (major_type << 5) | 25
    [major_byte].pack('C') + [length].pack('n')
  elsif length <= MAX_UINT32
    # 4-byte length follows
    major_byte = (major_type << 5) | 26
    [major_byte].pack('C') + [length].pack('N')
  elsif length <= MAX_UINT64
    # 8-byte length follows
    major_byte = (major_type << 5) | 27
    [major_byte].pack('C') + [length].pack('Q>')
  else
    raise EncodeError, "Length too large: #{length}"
  end
end

#handler_for(obj) ⇒ :encode_symbol, untyped

Parameters:

  • obj (Object)

Returns:

  • (:encode_symbol, untyped)

Raises:



103
104
105
106
107
108
109
110
111
# File 'lib/takagi/cbor/encoder.rb', line 103

def handler_for(obj)
  return :encode_symbol if obj.is_a?(Symbol)

  TYPE_HANDLERS.each do |klass, method|
    return method if obj.is_a?(klass)
  end

  raise EncodeError, "Cannot encode #{obj.class}: #{obj.inspect}"
end