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.
- checksums.yaml +4 -4
- data/.rubocop.yml +70 -7
- data/.yard/templates/default/layout/html/layout.erb +34 -0
- data/AGENTS.md +16 -0
- data/CHANGELOG.md +158 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +590 -23
- data/ROADMAP.md +55 -0
- data/Rakefile +4 -4
- data/Steepfile +39 -0
- data/bin/takagi-dev +159 -0
- data/docs/FIRST_PLUGIN_GUIDE.md +224 -0
- data/docs/HOOKS.md +31 -0
- data/examples/client_lifecycle_example.rb +118 -0
- data/examples/cloud_gateway_app.rb +217 -0
- data/examples/nested_api_app.rb +258 -0
- data/examples/simple_device_app.rb +71 -0
- data/examples/takagi.yml +138 -0
- data/lib/takagi/application.rb +256 -0
- data/lib/takagi/base/middleware_management.rb +39 -0
- data/lib/takagi/base/plugin_management.rb +75 -0
- data/lib/takagi/base/reactor_management.rb +104 -0
- data/lib/takagi/base/server_lifecycle.rb +156 -0
- data/lib/takagi/base.rb +103 -11
- data/lib/takagi/branding.rb +88 -0
- data/lib/takagi/cbor/decoder.rb +385 -0
- data/lib/takagi/cbor/encoder.rb +260 -0
- data/lib/takagi/cbor/error.rb +17 -0
- data/lib/takagi/cbor/version.rb +9 -0
- data/lib/takagi/client/response.rb +236 -0
- data/lib/takagi/client.rb +265 -0
- data/lib/takagi/client_base.rb +204 -0
- data/lib/takagi/coap/code_helpers.rb +190 -0
- data/lib/takagi/coap/registries/base.rb +165 -0
- data/lib/takagi/coap/registries/content_format.rb +71 -0
- data/lib/takagi/coap/registries/message_type.rb +69 -0
- data/lib/takagi/coap/registries/method.rb +38 -0
- data/lib/takagi/coap/registries/option.rb +71 -0
- data/lib/takagi/coap/registries/response.rb +93 -0
- data/lib/takagi/coap/registries/signaling.rb +34 -0
- data/lib/takagi/coap/signaling.rb +10 -0
- data/lib/takagi/coap.rb +37 -0
- data/lib/takagi/composite_router.rb +186 -0
- data/lib/takagi/config.rb +337 -0
- data/lib/takagi/controller/resource_allocator.rb +164 -0
- data/lib/takagi/controller/thread_pool.rb +144 -0
- data/lib/takagi/controller.rb +319 -0
- data/lib/takagi/core/attribute_set.rb +128 -0
- data/lib/takagi/discovery/core_link_format.rb +137 -0
- data/lib/takagi/errors.rb +536 -0
- data/lib/takagi/event_bus/address_prefix.rb +142 -0
- data/lib/takagi/event_bus/async_executor.rb +235 -0
- data/lib/takagi/event_bus/coap_bridge.rb +208 -0
- data/lib/takagi/event_bus/future.rb +153 -0
- data/lib/takagi/event_bus/lru_cache.rb +157 -0
- data/lib/takagi/event_bus/message_buffer.rb +237 -0
- data/lib/takagi/event_bus/observer_cleanup.rb +110 -0
- data/lib/takagi/event_bus/scope.rb +74 -0
- data/lib/takagi/event_bus.rb +594 -0
- data/lib/takagi/helpers.rb +88 -0
- data/lib/takagi/hooks.rb +82 -0
- data/lib/takagi/initializer.rb +18 -0
- data/lib/takagi/logger.rb +15 -6
- data/lib/takagi/message/base.rb +155 -0
- data/lib/takagi/message/deduplication_cache.rb +84 -0
- data/lib/takagi/message/inbound.rb +147 -0
- data/lib/takagi/message/outbound.rb +223 -0
- data/lib/takagi/message/request.rb +158 -0
- data/lib/takagi/message/retransmission_manager.rb +193 -0
- data/lib/takagi/middleware/authentication.rb +19 -0
- data/lib/takagi/middleware/caching.rb +23 -0
- data/lib/takagi/middleware/debugging.rb +16 -0
- data/lib/takagi/middleware/logging.rb +14 -0
- data/lib/takagi/middleware/metrics.rb +440 -0
- data/lib/takagi/middleware/rate_limiting.rb +24 -0
- data/lib/takagi/middleware_stack.rb +166 -0
- data/lib/takagi/network/base.rb +76 -0
- data/lib/takagi/network/framing/tcp.rb +222 -0
- data/lib/takagi/network/framing/udp.rb +110 -0
- data/lib/takagi/network/registry.rb +72 -0
- data/lib/takagi/network/tcp.rb +60 -0
- data/lib/takagi/network/tcp_sender.rb +21 -0
- data/lib/takagi/network/udp.rb +61 -0
- data/lib/takagi/network/udp_sender.rb +20 -0
- data/lib/takagi/observable/emitter.rb +62 -0
- data/lib/takagi/observable/reactor.rb +488 -0
- data/lib/takagi/observable/registry.rb +122 -0
- data/lib/takagi/observe_registry.rb +10 -0
- data/lib/takagi/observer/client.rb +68 -0
- data/lib/takagi/observer/registry.rb +137 -0
- data/lib/takagi/observer/sender.rb +39 -0
- data/lib/takagi/observer/watcher.rb +43 -0
- data/lib/takagi/plugin.rb +313 -0
- data/lib/takagi/profiles.rb +176 -0
- data/lib/takagi/reactor.rb +23 -0
- data/lib/takagi/reactor_registry.rb +64 -0
- data/lib/takagi/registry/base.rb +268 -0
- data/lib/takagi/response_builder.rb +141 -0
- data/lib/takagi/router/metadata_extractor.rb +133 -0
- data/lib/takagi/router/route_matcher.rb +83 -0
- data/lib/takagi/router.rb +284 -25
- data/lib/takagi/serialization/base.rb +102 -0
- data/lib/takagi/serialization/cbor_serializer.rb +92 -0
- data/lib/takagi/serialization/json_serializer.rb +96 -0
- data/lib/takagi/serialization/octet_stream_serializer.rb +82 -0
- data/lib/takagi/serialization/registry.rb +187 -0
- data/lib/takagi/serialization/text_serializer.rb +87 -0
- data/lib/takagi/serialization.rb +117 -0
- data/lib/takagi/server/multi.rb +41 -0
- data/lib/takagi/server/registry.rb +71 -0
- data/lib/takagi/server/tcp.rb +249 -0
- data/lib/takagi/server/udp.rb +139 -0
- data/lib/takagi/server/udp_worker.rb +174 -0
- data/lib/takagi/server.rb +1 -31
- data/lib/takagi/server_registry.rb +10 -0
- data/lib/takagi/tcp_client.rb +142 -0
- data/lib/takagi/version.rb +2 -1
- data/lib/takagi.rb +24 -3
- data/sig/takagi/application.rbs +48 -0
- data/sig/takagi/base/middleware_management.rbs +33 -0
- data/sig/takagi/base/reactor_management.rbs +52 -0
- data/sig/takagi/base/server_lifecycle.rbs +54 -0
- data/sig/takagi/base.rbs +48 -0
- data/sig/takagi/cbor/decoder.rbs +171 -0
- data/sig/takagi/cbor/encoder.rbs +146 -0
- data/sig/takagi/cbor/error.rbs +19 -0
- data/sig/takagi/cbor/version.rbs +7 -0
- data/sig/takagi/client/response.rbs +148 -0
- data/sig/takagi/client.rbs +119 -0
- data/sig/takagi/client_base.rbs +135 -0
- data/sig/takagi/coap/code_helpers.rbs +91 -0
- data/sig/takagi/coap/registries/base.rbs +95 -0
- data/sig/takagi/coap/registries/content_format.rbs +47 -0
- data/sig/takagi/coap/registries/message_type.rbs +53 -0
- data/sig/takagi/coap/registries/method.rbs +27 -0
- data/sig/takagi/coap/registries/option.rbs +43 -0
- data/sig/takagi/coap/registries/response.rbs +52 -0
- data/sig/takagi/coap.rbs +24 -0
- data/sig/takagi/composite_router.rbs +46 -0
- data/sig/takagi/config.rbs +134 -0
- data/sig/takagi/controller.rbs +73 -0
- data/sig/takagi/core/attribute_set.rbs +57 -0
- data/sig/takagi/discovery/core_link_format.rbs +50 -0
- data/sig/takagi/event_bus/address_prefix.rbs +78 -0
- data/sig/takagi/event_bus/async_executor.rbs +88 -0
- data/sig/takagi/event_bus/coap_bridge.rbs +93 -0
- data/sig/takagi/event_bus/future.rbs +78 -0
- data/sig/takagi/event_bus/lru_cache.rbs +86 -0
- data/sig/takagi/event_bus/message_buffer.rbs +133 -0
- data/sig/takagi/event_bus/observer_cleanup.rbs +62 -0
- data/sig/takagi/event_bus.rbs +320 -0
- data/sig/takagi/helpers.rbs +34 -0
- data/sig/takagi/initializer.rbs +9 -0
- data/sig/takagi/logger.rbs +17 -0
- data/sig/takagi/message/base.rbs +64 -0
- data/sig/takagi/message/deduplication_cache.rbs +49 -0
- data/sig/takagi/message/inbound.rbs +76 -0
- data/sig/takagi/message/outbound.rbs +48 -0
- data/sig/takagi/message/request.rbs +32 -0
- data/sig/takagi/message/retransmission_manager.rbs +76 -0
- data/sig/takagi/middleware/authentication.rbs +11 -0
- data/sig/takagi/middleware/caching.rbs +13 -0
- data/sig/takagi/middleware/debugging.rbs +9 -0
- data/sig/takagi/middleware/logging.rbs +7 -0
- data/sig/takagi/middleware/metrics.rbs +15 -0
- data/sig/takagi/middleware/rate_limiting.rbs +13 -0
- data/sig/takagi/middleware_stack.rbs +69 -0
- data/sig/takagi/network/tcp_sender.rbs +10 -0
- data/sig/takagi/network/udp_sender.rbs +14 -0
- data/sig/takagi/observe_registry.rbs +36 -0
- data/sig/takagi/observer/client.rbs +36 -0
- data/sig/takagi/observer/sender.rbs +12 -0
- data/sig/takagi/observer/watcher.rbs +18 -0
- data/sig/takagi/profiles.rbs +33 -0
- data/sig/takagi/reactor.rbs +20 -0
- data/sig/takagi/reactor_registry.rbs +14 -0
- data/sig/takagi/response_builder.rbs +12 -0
- data/sig/takagi/router/metadata_extractor.rbs +71 -0
- data/sig/takagi/router/route_matcher.rbs +43 -0
- data/sig/takagi/router.rbs +166 -0
- data/sig/takagi/serialization.rbs +32 -0
- data/sig/takagi/server/multi.rbs +16 -0
- data/sig/takagi/server/tcp.rbs +42 -0
- data/sig/takagi/server/udp.rbs +52 -0
- data/sig/takagi/server/udp_worker.rbs +42 -0
- data/sig/takagi/server.rbs +4 -0
- data/sig/takagi/server_registry.rbs +71 -0
- data/sig/takagi/tcp_client.rbs +23 -0
- data/sig/takagi/version.rbs +5 -0
- data/takagi.gemspec +37 -35
- metadata +204 -31
- data/.idea/.gitignore +0 -8
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/takagi.iml +0 -81
- data/.idea/vcs.xml +0 -6
- 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,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
|