takagi 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +70 -7
  3. data/.yard/templates/default/layout/html/layout.erb +34 -0
  4. data/AGENTS.md +16 -0
  5. data/CHANGELOG.md +158 -1
  6. data/CODE_OF_CONDUCT.md +1 -1
  7. data/README.md +590 -23
  8. data/ROADMAP.md +55 -0
  9. data/Rakefile +4 -4
  10. data/Steepfile +39 -0
  11. data/bin/takagi-dev +159 -0
  12. data/docs/FIRST_PLUGIN_GUIDE.md +224 -0
  13. data/docs/HOOKS.md +31 -0
  14. data/examples/client_lifecycle_example.rb +118 -0
  15. data/examples/cloud_gateway_app.rb +217 -0
  16. data/examples/nested_api_app.rb +258 -0
  17. data/examples/simple_device_app.rb +71 -0
  18. data/examples/takagi.yml +138 -0
  19. data/lib/takagi/application.rb +256 -0
  20. data/lib/takagi/base/middleware_management.rb +39 -0
  21. data/lib/takagi/base/plugin_management.rb +75 -0
  22. data/lib/takagi/base/reactor_management.rb +104 -0
  23. data/lib/takagi/base/server_lifecycle.rb +156 -0
  24. data/lib/takagi/base.rb +103 -11
  25. data/lib/takagi/branding.rb +88 -0
  26. data/lib/takagi/cbor/decoder.rb +385 -0
  27. data/lib/takagi/cbor/encoder.rb +260 -0
  28. data/lib/takagi/cbor/error.rb +17 -0
  29. data/lib/takagi/cbor/version.rb +9 -0
  30. data/lib/takagi/client/response.rb +236 -0
  31. data/lib/takagi/client.rb +265 -0
  32. data/lib/takagi/client_base.rb +204 -0
  33. data/lib/takagi/coap/code_helpers.rb +190 -0
  34. data/lib/takagi/coap/registries/base.rb +165 -0
  35. data/lib/takagi/coap/registries/content_format.rb +71 -0
  36. data/lib/takagi/coap/registries/message_type.rb +69 -0
  37. data/lib/takagi/coap/registries/method.rb +38 -0
  38. data/lib/takagi/coap/registries/option.rb +71 -0
  39. data/lib/takagi/coap/registries/response.rb +93 -0
  40. data/lib/takagi/coap/registries/signaling.rb +34 -0
  41. data/lib/takagi/coap/signaling.rb +10 -0
  42. data/lib/takagi/coap.rb +37 -0
  43. data/lib/takagi/composite_router.rb +186 -0
  44. data/lib/takagi/config.rb +337 -0
  45. data/lib/takagi/controller/resource_allocator.rb +164 -0
  46. data/lib/takagi/controller/thread_pool.rb +144 -0
  47. data/lib/takagi/controller.rb +319 -0
  48. data/lib/takagi/core/attribute_set.rb +128 -0
  49. data/lib/takagi/discovery/core_link_format.rb +137 -0
  50. data/lib/takagi/errors.rb +536 -0
  51. data/lib/takagi/event_bus/address_prefix.rb +142 -0
  52. data/lib/takagi/event_bus/async_executor.rb +235 -0
  53. data/lib/takagi/event_bus/coap_bridge.rb +208 -0
  54. data/lib/takagi/event_bus/future.rb +153 -0
  55. data/lib/takagi/event_bus/lru_cache.rb +157 -0
  56. data/lib/takagi/event_bus/message_buffer.rb +237 -0
  57. data/lib/takagi/event_bus/observer_cleanup.rb +110 -0
  58. data/lib/takagi/event_bus/scope.rb +74 -0
  59. data/lib/takagi/event_bus.rb +594 -0
  60. data/lib/takagi/helpers.rb +88 -0
  61. data/lib/takagi/hooks.rb +82 -0
  62. data/lib/takagi/initializer.rb +18 -0
  63. data/lib/takagi/logger.rb +15 -6
  64. data/lib/takagi/message/base.rb +155 -0
  65. data/lib/takagi/message/deduplication_cache.rb +84 -0
  66. data/lib/takagi/message/inbound.rb +147 -0
  67. data/lib/takagi/message/outbound.rb +223 -0
  68. data/lib/takagi/message/request.rb +158 -0
  69. data/lib/takagi/message/retransmission_manager.rb +193 -0
  70. data/lib/takagi/middleware/authentication.rb +19 -0
  71. data/lib/takagi/middleware/caching.rb +23 -0
  72. data/lib/takagi/middleware/debugging.rb +16 -0
  73. data/lib/takagi/middleware/logging.rb +14 -0
  74. data/lib/takagi/middleware/metrics.rb +440 -0
  75. data/lib/takagi/middleware/rate_limiting.rb +24 -0
  76. data/lib/takagi/middleware_stack.rb +166 -0
  77. data/lib/takagi/network/base.rb +76 -0
  78. data/lib/takagi/network/framing/tcp.rb +222 -0
  79. data/lib/takagi/network/framing/udp.rb +110 -0
  80. data/lib/takagi/network/registry.rb +72 -0
  81. data/lib/takagi/network/tcp.rb +60 -0
  82. data/lib/takagi/network/tcp_sender.rb +21 -0
  83. data/lib/takagi/network/udp.rb +61 -0
  84. data/lib/takagi/network/udp_sender.rb +20 -0
  85. data/lib/takagi/observable/emitter.rb +62 -0
  86. data/lib/takagi/observable/reactor.rb +488 -0
  87. data/lib/takagi/observable/registry.rb +122 -0
  88. data/lib/takagi/observe_registry.rb +10 -0
  89. data/lib/takagi/observer/client.rb +68 -0
  90. data/lib/takagi/observer/registry.rb +137 -0
  91. data/lib/takagi/observer/sender.rb +39 -0
  92. data/lib/takagi/observer/watcher.rb +43 -0
  93. data/lib/takagi/plugin.rb +313 -0
  94. data/lib/takagi/profiles.rb +176 -0
  95. data/lib/takagi/reactor.rb +23 -0
  96. data/lib/takagi/reactor_registry.rb +64 -0
  97. data/lib/takagi/registry/base.rb +268 -0
  98. data/lib/takagi/response_builder.rb +141 -0
  99. data/lib/takagi/router/metadata_extractor.rb +133 -0
  100. data/lib/takagi/router/route_matcher.rb +83 -0
  101. data/lib/takagi/router.rb +284 -25
  102. data/lib/takagi/serialization/base.rb +102 -0
  103. data/lib/takagi/serialization/cbor_serializer.rb +92 -0
  104. data/lib/takagi/serialization/json_serializer.rb +96 -0
  105. data/lib/takagi/serialization/octet_stream_serializer.rb +82 -0
  106. data/lib/takagi/serialization/registry.rb +187 -0
  107. data/lib/takagi/serialization/text_serializer.rb +87 -0
  108. data/lib/takagi/serialization.rb +117 -0
  109. data/lib/takagi/server/multi.rb +41 -0
  110. data/lib/takagi/server/registry.rb +71 -0
  111. data/lib/takagi/server/tcp.rb +249 -0
  112. data/lib/takagi/server/udp.rb +139 -0
  113. data/lib/takagi/server/udp_worker.rb +174 -0
  114. data/lib/takagi/server.rb +1 -31
  115. data/lib/takagi/server_registry.rb +10 -0
  116. data/lib/takagi/tcp_client.rb +142 -0
  117. data/lib/takagi/version.rb +2 -1
  118. data/lib/takagi.rb +24 -3
  119. data/sig/takagi/application.rbs +48 -0
  120. data/sig/takagi/base/middleware_management.rbs +33 -0
  121. data/sig/takagi/base/reactor_management.rbs +52 -0
  122. data/sig/takagi/base/server_lifecycle.rbs +54 -0
  123. data/sig/takagi/base.rbs +48 -0
  124. data/sig/takagi/cbor/decoder.rbs +171 -0
  125. data/sig/takagi/cbor/encoder.rbs +146 -0
  126. data/sig/takagi/cbor/error.rbs +19 -0
  127. data/sig/takagi/cbor/version.rbs +7 -0
  128. data/sig/takagi/client/response.rbs +148 -0
  129. data/sig/takagi/client.rbs +119 -0
  130. data/sig/takagi/client_base.rbs +135 -0
  131. data/sig/takagi/coap/code_helpers.rbs +91 -0
  132. data/sig/takagi/coap/registries/base.rbs +95 -0
  133. data/sig/takagi/coap/registries/content_format.rbs +47 -0
  134. data/sig/takagi/coap/registries/message_type.rbs +53 -0
  135. data/sig/takagi/coap/registries/method.rbs +27 -0
  136. data/sig/takagi/coap/registries/option.rbs +43 -0
  137. data/sig/takagi/coap/registries/response.rbs +52 -0
  138. data/sig/takagi/coap.rbs +24 -0
  139. data/sig/takagi/composite_router.rbs +46 -0
  140. data/sig/takagi/config.rbs +134 -0
  141. data/sig/takagi/controller.rbs +73 -0
  142. data/sig/takagi/core/attribute_set.rbs +57 -0
  143. data/sig/takagi/discovery/core_link_format.rbs +50 -0
  144. data/sig/takagi/event_bus/address_prefix.rbs +78 -0
  145. data/sig/takagi/event_bus/async_executor.rbs +88 -0
  146. data/sig/takagi/event_bus/coap_bridge.rbs +93 -0
  147. data/sig/takagi/event_bus/future.rbs +78 -0
  148. data/sig/takagi/event_bus/lru_cache.rbs +86 -0
  149. data/sig/takagi/event_bus/message_buffer.rbs +133 -0
  150. data/sig/takagi/event_bus/observer_cleanup.rbs +62 -0
  151. data/sig/takagi/event_bus.rbs +320 -0
  152. data/sig/takagi/helpers.rbs +34 -0
  153. data/sig/takagi/initializer.rbs +9 -0
  154. data/sig/takagi/logger.rbs +17 -0
  155. data/sig/takagi/message/base.rbs +64 -0
  156. data/sig/takagi/message/deduplication_cache.rbs +49 -0
  157. data/sig/takagi/message/inbound.rbs +76 -0
  158. data/sig/takagi/message/outbound.rbs +48 -0
  159. data/sig/takagi/message/request.rbs +32 -0
  160. data/sig/takagi/message/retransmission_manager.rbs +76 -0
  161. data/sig/takagi/middleware/authentication.rbs +11 -0
  162. data/sig/takagi/middleware/caching.rbs +13 -0
  163. data/sig/takagi/middleware/debugging.rbs +9 -0
  164. data/sig/takagi/middleware/logging.rbs +7 -0
  165. data/sig/takagi/middleware/metrics.rbs +15 -0
  166. data/sig/takagi/middleware/rate_limiting.rbs +13 -0
  167. data/sig/takagi/middleware_stack.rbs +69 -0
  168. data/sig/takagi/network/tcp_sender.rbs +10 -0
  169. data/sig/takagi/network/udp_sender.rbs +14 -0
  170. data/sig/takagi/observe_registry.rbs +36 -0
  171. data/sig/takagi/observer/client.rbs +36 -0
  172. data/sig/takagi/observer/sender.rbs +12 -0
  173. data/sig/takagi/observer/watcher.rbs +18 -0
  174. data/sig/takagi/profiles.rbs +33 -0
  175. data/sig/takagi/reactor.rbs +20 -0
  176. data/sig/takagi/reactor_registry.rbs +14 -0
  177. data/sig/takagi/response_builder.rbs +12 -0
  178. data/sig/takagi/router/metadata_extractor.rbs +71 -0
  179. data/sig/takagi/router/route_matcher.rbs +43 -0
  180. data/sig/takagi/router.rbs +166 -0
  181. data/sig/takagi/serialization.rbs +32 -0
  182. data/sig/takagi/server/multi.rbs +16 -0
  183. data/sig/takagi/server/tcp.rbs +42 -0
  184. data/sig/takagi/server/udp.rbs +52 -0
  185. data/sig/takagi/server/udp_worker.rbs +42 -0
  186. data/sig/takagi/server.rbs +4 -0
  187. data/sig/takagi/server_registry.rbs +71 -0
  188. data/sig/takagi/tcp_client.rbs +23 -0
  189. data/sig/takagi/version.rbs +5 -0
  190. data/takagi.gemspec +37 -35
  191. metadata +204 -31
  192. data/.idea/.gitignore +0 -8
  193. data/.idea/misc.xml +0 -4
  194. data/.idea/modules.xml +0 -8
  195. data/.idea/takagi.iml +0 -81
  196. data/.idea/vcs.xml +0 -6
  197. data/lib/takagi/message.rb +0 -75
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ module CBOR
5
+ # CBOR Encoder (RFC 8949)
6
+ #
7
+ # Encodes Ruby objects to CBOR binary format.
8
+ # Optimized for IoT/CoAP workloads with minimal footprint.
9
+ #
10
+ # Supported types:
11
+ # - Integers (signed/unsigned, up to 64-bit)
12
+ # - Floats (64-bit IEEE 754)
13
+ # - Strings (UTF-8)
14
+ # - Byte strings (binary data)
15
+ # - Arrays
16
+ # - Hashes (maps)
17
+ # - Booleans (true/false)
18
+ # - nil (null)
19
+ # - Time (timestamp, tag 1)
20
+ #
21
+ # @example Basic encoding
22
+ # Encoder.encode({ temperature: 25.5, humidity: 60 })
23
+ # # => "\xA2ktempera..." (CBOR bytes)
24
+ #
25
+ # @example Encoding with symbols
26
+ # Encoder.encode({ temp: 25.5 })
27
+ # # Symbols converted to strings
28
+ class Encoder
29
+ # CBOR Major Types (RFC 8949 §3)
30
+ MAJOR_TYPE_UNSIGNED_INT = 0
31
+ MAJOR_TYPE_NEGATIVE_INT = 1
32
+ MAJOR_TYPE_BYTE_STRING = 2
33
+ MAJOR_TYPE_TEXT_STRING = 3
34
+ MAJOR_TYPE_ARRAY = 4
35
+ MAJOR_TYPE_MAP = 5
36
+ MAJOR_TYPE_TAG = 6
37
+ MAJOR_TYPE_SIMPLE = 7
38
+
39
+ # Simple values (RFC 8949 §3.3)
40
+ SIMPLE_FALSE = 20
41
+ SIMPLE_TRUE = 21
42
+ SIMPLE_NULL = 22
43
+ SIMPLE_FLOAT64 = 27
44
+
45
+ # Tag values (RFC 8949 §3.4)
46
+ TAG_EPOCH_TIMESTAMP = 1
47
+
48
+ # Maximum safe integer values
49
+ MAX_UINT8 = 0xFF
50
+ MAX_UINT16 = 0xFFFF
51
+ MAX_UINT32 = 0xFFFFFFFF
52
+ MAX_UINT64 = 0xFFFFFFFFFFFFFFFF
53
+
54
+ TYPE_HANDLERS = {
55
+ Integer => :encode_integer,
56
+ Float => :encode_float,
57
+ String => :encode_string,
58
+ Array => :encode_array,
59
+ Hash => :encode_map,
60
+ TrueClass => :encode_simple,
61
+ FalseClass => :encode_simple,
62
+ NilClass => :encode_simple,
63
+ Time => :encode_timestamp
64
+ }.freeze
65
+
66
+ class << self
67
+ # Encode a Ruby object to CBOR bytes
68
+ #
69
+ # @param obj [Object] Ruby object to encode
70
+ # @return [String] CBOR-encoded binary string
71
+ # @raise [EncodeError] if object cannot be encoded
72
+ #
73
+ # @example
74
+ # Encoder.encode(42) # => "\x18\x2A"
75
+ # Encoder.encode("hello") # => "ehello"
76
+ # Encoder.encode([1, 2, 3]) # => "\x83\x01\x02\x03"
77
+ # Encoder.encode({ a: 1 }) # => "\xA1aa\x01"
78
+ def encode(obj)
79
+ new.encode(obj)
80
+ end
81
+ end
82
+
83
+ # Encode a Ruby object to CBOR bytes
84
+ #
85
+ # @param obj [Object] Ruby object to encode
86
+ # @return [String] CBOR-encoded binary string
87
+ # @raise [EncodeError] if object cannot be encoded
88
+ def encode(obj)
89
+ encode_value(obj)
90
+ end
91
+
92
+ private
93
+
94
+ def encode_value(obj)
95
+ handler = handler_for(obj)
96
+ send(handler, obj)
97
+ rescue EncodeError
98
+ raise
99
+ rescue StandardError => e
100
+ raise EncodeError, "Encoding failed: #{e.message}"
101
+ end
102
+
103
+ def handler_for(obj)
104
+ return :encode_symbol if obj.is_a?(Symbol)
105
+
106
+ TYPE_HANDLERS.each do |klass, method|
107
+ return method if obj.is_a?(klass)
108
+ end
109
+
110
+ raise EncodeError, "Cannot encode #{obj.class}: #{obj.inspect}"
111
+ end
112
+
113
+ def encode_symbol(symbol)
114
+ encode_string(symbol.to_s)
115
+ end
116
+
117
+ # Encode integer (major type 0 or 1)
118
+ def encode_integer(int)
119
+ if int >= 0
120
+ encode_unsigned_int(int)
121
+ else
122
+ encode_negative_int(int)
123
+ end
124
+ end
125
+
126
+ # Encode unsigned integer (major type 0)
127
+ # RFC 8949 §3.1
128
+ def encode_unsigned_int(int)
129
+ raise EncodeError, "Integer too large: #{int}" if int > MAX_UINT64
130
+
131
+ encode_with_length(MAJOR_TYPE_UNSIGNED_INT, int)
132
+ end
133
+
134
+ # Encode negative integer (major type 1)
135
+ # RFC 8949 §3.1: -1 - n
136
+ def encode_negative_int(int)
137
+ # Convert to CBOR representation: -1 - n
138
+ # Example: -1 => 0, -2 => 1, -500 => 499
139
+ n = -1 - int
140
+
141
+ raise EncodeError, "Integer too small: #{int}" if n > MAX_UINT64
142
+
143
+ encode_with_length(MAJOR_TYPE_NEGATIVE_INT, n)
144
+ end
145
+
146
+ # Encode float (major type 7, additional info 27)
147
+ # RFC 8949 §3.3: Always use 64-bit IEEE 754
148
+ def encode_float(float)
149
+ # Major type 7, additional info 27 (64-bit float)
150
+ major_byte = (MAJOR_TYPE_SIMPLE << 5) | SIMPLE_FLOAT64
151
+
152
+ # Pack as big-endian 64-bit float (network byte order)
153
+ [major_byte].pack('C') + [float].pack('G')
154
+ end
155
+
156
+ # Encode UTF-8 string (major type 3)
157
+ # RFC 8949 §3.1
158
+ def encode_string(str)
159
+ # Ensure UTF-8 encoding
160
+ utf8_str = str.encode('UTF-8')
161
+ byte_length = utf8_str.bytesize
162
+
163
+ encode_with_length(MAJOR_TYPE_TEXT_STRING, byte_length) + utf8_str
164
+ end
165
+
166
+ # Encode byte string (major type 2)
167
+ # RFC 8949 §3.1
168
+ def encode_byte_string(bytes)
169
+ byte_length = bytes.bytesize
170
+
171
+ encode_with_length(MAJOR_TYPE_BYTE_STRING, byte_length) + bytes
172
+ end
173
+
174
+ # Encode array (major type 4)
175
+ # RFC 8949 §3.1
176
+ def encode_array(arr)
177
+ result = encode_with_length(MAJOR_TYPE_ARRAY, arr.size)
178
+
179
+ arr.each do |item|
180
+ result << encode_value(item)
181
+ end
182
+
183
+ result
184
+ end
185
+
186
+ # Encode map/hash (major type 5)
187
+ # RFC 8949 §3.1
188
+ def encode_map(hash)
189
+ result = encode_with_length(MAJOR_TYPE_MAP, hash.size)
190
+
191
+ hash.each do |key, value|
192
+ result << encode_value(key)
193
+ result << encode_value(value)
194
+ end
195
+
196
+ result
197
+ end
198
+
199
+ # Encode simple values (major type 7)
200
+ # RFC 8949 §3.3
201
+ def encode_simple(obj)
202
+ simple_value = case obj
203
+ when false then SIMPLE_FALSE
204
+ when true then SIMPLE_TRUE
205
+ when nil then SIMPLE_NULL
206
+ end
207
+
208
+ major_byte = (MAJOR_TYPE_SIMPLE << 5) | simple_value
209
+ [major_byte].pack('C')
210
+ end
211
+
212
+ # Encode timestamp (tag 1, epoch seconds)
213
+ # RFC 8949 §3.4.2
214
+ def encode_timestamp(time)
215
+ # Tag 1: Epoch-based timestamp (integer seconds since 1970-01-01)
216
+ tag_byte = encode_with_length(MAJOR_TYPE_TAG, TAG_EPOCH_TIMESTAMP)
217
+
218
+ # Encode timestamp as integer (seconds since epoch)
219
+ timestamp_int = time.to_i
220
+
221
+ tag_byte + encode_value(timestamp_int)
222
+ end
223
+
224
+ # Encode major type with length/value
225
+ # RFC 8949 §3: Additional Information encoding
226
+ #
227
+ # Additional info:
228
+ # 0-23: Value directly in additional info
229
+ # 24: 1-byte uint8 follows
230
+ # 25: 2-byte uint16 follows
231
+ # 26: 4-byte uint32 follows
232
+ # 27: 8-byte uint64 follows
233
+ def encode_with_length(major_type, length)
234
+ if length < 24
235
+ # Value fits in additional info (0-23)
236
+ major_byte = (major_type << 5) | length
237
+ [major_byte].pack('C')
238
+ elsif length <= MAX_UINT8
239
+ # 1-byte length follows (24-255)
240
+ major_byte = (major_type << 5) | 24
241
+ [major_byte, length].pack('CC')
242
+ elsif length <= MAX_UINT16
243
+ # 2-byte length follows (256-65535)
244
+ major_byte = (major_type << 5) | 25
245
+ [major_byte].pack('C') + [length].pack('n')
246
+ elsif length <= MAX_UINT32
247
+ # 4-byte length follows
248
+ major_byte = (major_type << 5) | 26
249
+ [major_byte].pack('C') + [length].pack('N')
250
+ elsif length <= MAX_UINT64
251
+ # 8-byte length follows
252
+ major_byte = (major_type << 5) | 27
253
+ [major_byte].pack('C') + [length].pack('Q>')
254
+ else
255
+ raise EncodeError, "Length too large: #{length}"
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ module CBOR
5
+ # Base error for all CBOR-related errors
6
+ class Error < StandardError; end
7
+
8
+ # Raised when encoding fails
9
+ class EncodeError < Error; end
10
+
11
+ # Raised when decoding fails
12
+ class DecodeError < Error; end
13
+
14
+ # Raised when encountering unsupported CBOR features
15
+ class UnsupportedError < Error; end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ module CBOR
5
+ # Version of the CBOR implementation
6
+ # Separate version for future gem extraction
7
+ VERSION = '0.1.0'
8
+ end
9
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ class Client
5
+ # Wrapper for CoAP responses providing convenient access to response data
6
+ # and status checking methods.
7
+ #
8
+ # Uses the CoAP registry system for all code checking and naming.
9
+ #
10
+ # @example Basic usage
11
+ # client.get('/temperature') do |response|
12
+ # if response.success?
13
+ # puts "Temperature: #{response.payload}"
14
+ # else
15
+ # puts "Error: #{response.code_name}"
16
+ # end
17
+ # end
18
+ #
19
+ # @example Checking specific codes
20
+ # response.ok? # 2.05 Content
21
+ # response.created? # 2.01 Created
22
+ # response.not_found? # 4.04 Not Found
23
+ # response.bad_request? # 4.00 Bad Request
24
+ class Response
25
+ attr_reader :raw_data, :inbound, :code, :payload, :options, :token
26
+
27
+ # Creates a new Response wrapper
28
+ # @param raw_data [String] Raw binary response data
29
+ def initialize(raw_data)
30
+ @raw_data = raw_data
31
+ @inbound = Takagi::Message::Inbound.new(raw_data)
32
+ @code = @inbound.code
33
+ @payload = @inbound.payload
34
+ @options = @inbound.options
35
+ @token = @inbound.token
36
+ end
37
+
38
+ # Get the human-readable code name using CoAP registry
39
+ # @return [String] Code name (e.g., "2.05 Content", "4.04 Not Found")
40
+ def code_name
41
+ CoAP::CodeHelpers.to_string(@code)
42
+ end
43
+
44
+ # Get the numeric code class (2 = Success, 4 = Client Error, 5 = Server Error)
45
+ # @return [Integer] Code class
46
+ def code_class
47
+ CoAP::Registries::Response.class_for(@code)
48
+ end
49
+
50
+ # Check if response is successful (2.xx)
51
+ # @return [Boolean]
52
+ def success?
53
+ CoAP::Registries::Response.success?(@code)
54
+ end
55
+
56
+ # Check if response is a client error (4.xx)
57
+ # @return [Boolean]
58
+ def client_error?
59
+ CoAP::Registries::Response.client_error?(@code)
60
+ end
61
+
62
+ # Check if response is a server error (5.xx)
63
+ # @return [Boolean]
64
+ def server_error?
65
+ CoAP::Registries::Response.server_error?(@code)
66
+ end
67
+
68
+ # Check if response has an error (4.xx or 5.xx)
69
+ # @return [Boolean]
70
+ def error?
71
+ CoAP::Registries::Response.error?(@code)
72
+ end
73
+
74
+ # Common 2.xx success codes (using registry)
75
+ def created?
76
+ @code == CoAP::Registries::Response::CREATED
77
+ end
78
+
79
+ def deleted?
80
+ @code == CoAP::Registries::Response::DELETED
81
+ end
82
+
83
+ def valid?
84
+ @code == CoAP::Registries::Response::VALID
85
+ end
86
+
87
+ def changed?
88
+ @code == CoAP::Registries::Response::CHANGED
89
+ end
90
+
91
+ def content?
92
+ @code == CoAP::Registries::Response::CONTENT
93
+ end
94
+ alias ok? content?
95
+
96
+ # Common 4.xx client error codes (using registry)
97
+ def bad_request?
98
+ @code == CoAP::Registries::Response::BAD_REQUEST
99
+ end
100
+
101
+ def unauthorized?
102
+ @code == CoAP::Registries::Response::UNAUTHORIZED
103
+ end
104
+
105
+ def bad_option?
106
+ @code == CoAP::Registries::Response::BAD_OPTION
107
+ end
108
+
109
+ def forbidden?
110
+ @code == CoAP::Registries::Response::FORBIDDEN
111
+ end
112
+
113
+ def not_found?
114
+ @code == CoAP::Registries::Response::NOT_FOUND
115
+ end
116
+
117
+ def method_not_allowed?
118
+ @code == CoAP::Registries::Response::METHOD_NOT_ALLOWED
119
+ end
120
+
121
+ def not_acceptable?
122
+ @code == CoAP::Registries::Response::NOT_ACCEPTABLE
123
+ end
124
+
125
+ def precondition_failed?
126
+ @code == CoAP::Registries::Response::PRECONDITION_FAILED
127
+ end
128
+
129
+ def request_entity_too_large?
130
+ @code == CoAP::Registries::Response::REQUEST_ENTITY_TOO_LARGE
131
+ end
132
+
133
+ def unsupported_content_format?
134
+ @code == CoAP::Registries::Response::UNSUPPORTED_CONTENT_FORMAT
135
+ end
136
+
137
+ # Common 5.xx server error codes (using registry)
138
+ def internal_server_error?
139
+ @code == CoAP::Registries::Response::INTERNAL_SERVER_ERROR
140
+ end
141
+
142
+ def not_implemented?
143
+ @code == CoAP::Registries::Response::NOT_IMPLEMENTED
144
+ end
145
+
146
+ def bad_gateway?
147
+ @code == CoAP::Registries::Response::BAD_GATEWAY
148
+ end
149
+
150
+ def service_unavailable?
151
+ @code == CoAP::Registries::Response::SERVICE_UNAVAILABLE
152
+ end
153
+
154
+ def gateway_timeout?
155
+ @code == CoAP::Registries::Response::GATEWAY_TIMEOUT
156
+ end
157
+
158
+ def proxying_not_supported?
159
+ @code == CoAP::Registries::Response::PROXYING_NOT_SUPPORTED
160
+ end
161
+
162
+ # Deserialize payload using content-format
163
+ #
164
+ # Automatically deserializes payload based on Content-Format option.
165
+ # Falls back to JSON for unknown formats or if deserialization fails.
166
+ #
167
+ # @return [Object, nil] Deserialized data or nil
168
+ #
169
+ # @example JSON response
170
+ # response.data # => { "temp" => 25 }
171
+ #
172
+ # @example CBOR response
173
+ # response.data # => { "temp" => 25 }
174
+ #
175
+ # @example Text response
176
+ # response.data # => "Hello World"
177
+ def data
178
+ return nil unless @payload
179
+
180
+ format = content_format || CoAP::Registries::ContentFormat::JSON
181
+
182
+ Serialization::Registry.decode(@payload, format)
183
+ rescue Serialization::UnknownFormatError
184
+ # Unknown format - try JSON as fallback
185
+ Serialization::Registry.decode(@payload, CoAP::Registries::ContentFormat::JSON)
186
+ rescue Serialization::DecodeError
187
+ # Decoding failed - return raw payload
188
+ @payload
189
+ end
190
+
191
+ # Check if response has JSON content-format
192
+ # @return [Boolean]
193
+ def json?
194
+ content_format == CoAP::Registries::ContentFormat::JSON
195
+ end
196
+
197
+ # Get content-format option value
198
+ # @return [Integer, nil] Content-format code
199
+ def content_format
200
+ return nil unless @options
201
+
202
+ value = @options[CoAP::Registries::Option::CONTENT_FORMAT]
203
+ return nil if value.nil?
204
+
205
+ # Handle both array and non-array values
206
+ value = value.first if value.is_a?(Array)
207
+
208
+ # Convert to integer (content-format is numeric)
209
+ value.is_a?(String) ? decode_integer_value(value) : value
210
+ end
211
+
212
+ # String representation for debugging
213
+ # @return [String]
214
+ def to_s
215
+ "#<Takagi::Client::Response code=#{code_name} payload_size=#{@payload&.bytesize || 0}>"
216
+ end
217
+
218
+ # Detailed inspection
219
+ # @return [String]
220
+ def inspect
221
+ "#<Takagi::Client::Response code=#{code_name} " \
222
+ "success=#{success?} " \
223
+ "payload=#{@payload&.byteslice(0, 50)&.inspect}#{@payload && @payload.bytesize > 50 ? '...' : ''}>"
224
+ end
225
+
226
+ private
227
+
228
+ # Decode a binary string to an integer
229
+ def decode_integer_value(bytes)
230
+ return nil if bytes.nil? || bytes.empty?
231
+
232
+ bytes.bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
233
+ end
234
+ end
235
+ end
236
+ end