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
data/lib/takagi/base.rb CHANGED
@@ -1,18 +1,110 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
- require "sequel"
5
- require "socket"
6
- require "json"
7
-
8
- require_relative "router"
9
- require_relative "message"
10
- require_relative "server"
3
+ require 'rack'
4
+ require 'socket'
5
+ require 'json'
6
+ require_relative 'server/multi'
7
+ require_relative 'server_registry'
8
+ require_relative 'network/registry'
9
+ require_relative 'network/udp'
10
+ require_relative 'network/tcp'
11
+ require_relative 'plugin'
12
+ require_relative 'base/plugin_management'
11
13
 
12
14
  module Takagi
13
- class Base < Takagi::Router
14
- def self.run!(port: 5683)
15
- Takagi::Server.run!(port: port)
15
+ # Base class that every Takagi based app should use.
16
+ #
17
+ # Provides a Sinatra-like DSL for building CoAP servers with support for:
18
+ # - Route registration (GET, POST, PUT, DELETE, OBSERVE)
19
+ # - Middleware stack
20
+ # - Reactor pattern for observables/observers
21
+ # - Multi-protocol servers (UDP, TCP)
22
+ #
23
+ # This class now follows Single Responsibility Principle by delegating
24
+ # specific concerns to focused modules:
25
+ # - ServerLifecycle: Boot, run, spawn operations
26
+ # - MiddlewareManagement: Middleware stack configuration
27
+ # - ReactorManagement: Observable/observer patterns
28
+ #
29
+ # @example Basic usage
30
+ # class MyAPI < Takagi::Base
31
+ # get '/temperature' do
32
+ # { value: 25.5, unit: 'C' }
33
+ # end
34
+ # end
35
+ #
36
+ # MyAPI.run!
37
+ class Base < Router
38
+ extend ServerLifecycle
39
+ extend MiddlewareManagement
40
+ extend ReactorManagement
41
+ extend PluginManagement
42
+
43
+ # Returns the global router instance
44
+ #
45
+ # @return [Router] Singleton router instance
46
+ def self.router
47
+ @router ||= Takagi::Router.instance
48
+ end
49
+
50
+ # Dynamically delegate route registration methods to router
51
+ # Generates: get, post, put, delete, fetch, etc. from CoAP::Registries::Method registry
52
+ CoAP::Registries::Method.all.each_value do |method_name|
53
+ method_string = method_name.split.first
54
+ method_symbol = method_string.downcase.to_sym
55
+
56
+ define_singleton_method(method_symbol) do |path, metadata: {}, &block|
57
+ router.public_send(method_symbol, path, metadata: metadata, &block)
58
+ end
59
+ end
60
+
61
+ # Registers an OBSERVE route in the global router (server-side)
62
+ # Use this to make a resource observable by clients
63
+ # @param path [String] The URL path
64
+ # @param block [Proc] The handler function
65
+ def self.observable(path, metadata: {}, &block)
66
+ router.observable(path, metadata: metadata, &block)
67
+ end
68
+
69
+ # Configures CoRE Link Format metadata for an existing route. Handy when
70
+ # you want to declare handlers and metadata separately (e.g., during boot).
71
+ def self.core(path, method: :get, &block)
72
+ router.configure_core(method.to_s.upcase, path, &block)
16
73
  end
74
+
75
+ # Default routes for basic functionality and RFC 6690 discovery
76
+ get '/.well-known/core', metadata: {
77
+ rt: 'core.discovery',
78
+ if: 'core.rd',
79
+ ct: Takagi::Discovery::CoreLinkFormat::CONTENT_FORMAT,
80
+ discovery: true,
81
+ title: 'Resource Discovery'
82
+ } do |req|
83
+ payload = Takagi::Discovery::CoreLinkFormat.generate(router: router, request: req)
84
+ req.to_response(
85
+ '2.05 Content',
86
+ payload,
87
+ options: { CoAP::Registries::Option::CONTENT_FORMAT => Takagi::Discovery::CoreLinkFormat::CONTENT_FORMAT }
88
+ )
89
+ end
90
+
91
+ get '/ping' do
92
+ { message: 'Pong' }
93
+ end
94
+
95
+ post '/echo' do |req|
96
+ body = JSON.parse(req.payload || '{}')
97
+ { echo: body['message'] }
98
+ rescue JSON::ParserError
99
+ { error: 'Invalid JSON' }
100
+ end
101
+
102
+ # Register default server implementations
103
+ Server::Registry.register(:udp, Takagi::Server::Udp, rfc: 'RFC 7252')
104
+ Server::Registry.register(:tcp, Takagi::Server::Tcp, rfc: 'RFC 8323')
105
+
106
+ # Register default transport implementations
107
+ Network::Registry.register(:udp, Network::Udp)
108
+ Network::Registry.register(:tcp, Network::Tcp)
17
109
  end
18
110
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ # Branding and visual identity for Takagi
5
+ module Branding
6
+ # Japanese wave symbol - represents observables, events, and flow
7
+ WAVE = '波'
8
+ LOGO = "[#{WAVE}]"
9
+
10
+ # Styled logo with name
11
+ LOGO_WITH_NAME = "#{LOGO} Takagi"
12
+
13
+ # ASCII art banner for server startup
14
+ BANNER = <<~BANNER
15
+
16
+ ╔═══════════════════════════════════╗
17
+ ║ ║
18
+ ║ [#{WAVE}] T A K A G I ║
19
+ ║ ║
20
+ ║ CoAP Framework for Ruby ║
21
+ ║ ║
22
+ ╚═══════════════════════════════════╝
23
+
24
+ BANNER
25
+
26
+ # Compact banner for constrained terminals
27
+ COMPACT_BANNER = <<~BANNER
28
+ ═══════════════════════════════
29
+ [#{WAVE}] Takagi CoAP Framework
30
+ ═══════════════════════════════
31
+ BANNER
32
+
33
+ # Wave pattern decorations
34
+ WAVE_LINE = '〜' * 20
35
+
36
+ # Prefix for log messages
37
+ # @param message [String] Log message
38
+ # @return [String] Message with logo prefix
39
+ def self.log(message)
40
+ "#{LOGO} #{message}"
41
+ end
42
+
43
+ # Print startup banner
44
+ # @param compact [Boolean] Use compact banner for smaller terminals
45
+ def self.print_banner(compact: false)
46
+ puts compact ? COMPACT_BANNER : BANNER
47
+ end
48
+
49
+ # Print version info
50
+ # @param version [String] Version string
51
+ def self.print_version(version)
52
+ puts "#{LOGO_WITH_NAME} v#{version}"
53
+ end
54
+
55
+ # Styled section header
56
+ # @param title [String] Section title
57
+ # @return [String] Styled header
58
+ def self.section(title)
59
+ <<~SECTION
60
+
61
+ #{WAVE_LINE}
62
+ #{LOGO} #{title}
63
+ #{WAVE_LINE}
64
+ SECTION
65
+ end
66
+
67
+ # Error prefix with wave
68
+ # @param message [String] Error message
69
+ # @return [String] Styled error
70
+ def self.error(message)
71
+ "#{LOGO} ERROR: #{message}"
72
+ end
73
+
74
+ # Success prefix with wave
75
+ # @param message [String] Success message
76
+ # @return [String] Styled success
77
+ def self.success(message)
78
+ "#{LOGO} ✓ #{message}"
79
+ end
80
+
81
+ # Info prefix with wave
82
+ # @param message [String] Info message
83
+ # @return [String] Styled info
84
+ def self.info(message)
85
+ "#{LOGO} #{message}"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ module CBOR
5
+ # CBOR Decoder (RFC 8949)
6
+ #
7
+ # Decodes CBOR binary format to Ruby objects.
8
+ # Optimized for IoT/CoAP workloads with minimal footprint.
9
+ #
10
+ # Supported types:
11
+ # - Integers → Integer
12
+ # - Floats → Float
13
+ # - Text strings → String (UTF-8)
14
+ # - Byte strings → String (binary)
15
+ # - Arrays → Array
16
+ # - Maps → Hash (string keys)
17
+ # - Booleans → true/false
18
+ # - null → nil
19
+ # - Timestamps (tag 1) → Time
20
+ #
21
+ # @example Basic decoding
22
+ # Decoder.decode("\xA2ktempera...")
23
+ # # => { "temperature" => 25.5, "humidity" => 60 }
24
+ #
25
+ # Security features:
26
+ # - Max nesting depth (prevents stack overflow)
27
+ # - Max collection size (prevents memory exhaustion)
28
+ class Decoder # rubocop:disable Metrics/ClassLength
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_FLOAT16 = 25
44
+ SIMPLE_FLOAT32 = 26
45
+ SIMPLE_FLOAT64 = 27
46
+
47
+ # Security limits
48
+ MAX_NESTING_DEPTH = 100
49
+ MAX_COLLECTION_SIZE = 100_000
50
+
51
+ # Tag values
52
+ TAG_EPOCH_TIMESTAMP = 1
53
+
54
+ MAJOR_TYPE_HANDLERS = {
55
+ MAJOR_TYPE_UNSIGNED_INT => :handle_unsigned_int,
56
+ MAJOR_TYPE_NEGATIVE_INT => :handle_negative_int,
57
+ MAJOR_TYPE_BYTE_STRING => :read_bytes,
58
+ MAJOR_TYPE_TEXT_STRING => :read_string,
59
+ MAJOR_TYPE_ARRAY => :read_array,
60
+ MAJOR_TYPE_MAP => :read_map,
61
+ MAJOR_TYPE_TAG => :read_tagged,
62
+ MAJOR_TYPE_SIMPLE => :read_simple
63
+ }.freeze
64
+
65
+ class << self
66
+ # Decode CBOR bytes to Ruby object
67
+ #
68
+ # @param bytes [String] CBOR-encoded binary string
69
+ # @return [Object] Decoded Ruby object
70
+ # @raise [DecodeError] if bytes cannot be decoded
71
+ #
72
+ # @example
73
+ # Decoder.decode("\x18\x2A") # => 42
74
+ # Decoder.decode("ehello") # => "hello"
75
+ # Decoder.decode("\x83\x01\x02\x03") # => [1, 2, 3]
76
+ def decode(bytes)
77
+ new(bytes).decode
78
+ end
79
+ end
80
+
81
+ # Initialize decoder with CBOR bytes
82
+ #
83
+ # @param bytes [String] CBOR-encoded binary string
84
+ def initialize(bytes)
85
+ @bytes = bytes.b
86
+ @pos = 0
87
+ @depth = 0
88
+ end
89
+
90
+ # Decode CBOR bytes to Ruby object
91
+ #
92
+ # @return [Object] Decoded Ruby object
93
+ # @raise [DecodeError] if bytes cannot be decoded
94
+ def decode
95
+ check_depth!
96
+
97
+ major_type, value = read_type_and_value
98
+ handler = MAJOR_TYPE_HANDLERS[major_type]
99
+ raise DecodeError, "Unknown major type: #{major_type}" unless handler
100
+
101
+ send(handler, value)
102
+ rescue DecodeError, UnsupportedError
103
+ raise
104
+ rescue StandardError => e
105
+ raise DecodeError, "Decoding failed at position #{@pos}: #{e.message}"
106
+ end
107
+
108
+ private
109
+
110
+ def handle_unsigned_int(value)
111
+ value
112
+ end
113
+
114
+ def handle_negative_int(value)
115
+ -1 - value
116
+ end
117
+
118
+ # Check nesting depth to prevent stack overflow
119
+ def check_depth!
120
+ return if @depth < MAX_NESTING_DEPTH
121
+
122
+ raise DecodeError, "Maximum nesting depth exceeded (#{MAX_NESTING_DEPTH})"
123
+ end
124
+
125
+ # Read major type and additional value
126
+ # RFC 8949 §3: Initial byte encoding
127
+ #
128
+ # Returns [major_type, value]
129
+ # - major_type: 0-7 (3 bits)
130
+ # - value: depends on additional info (5 bits)
131
+ def read_type_and_value
132
+ raise DecodeError, 'Unexpected end of input' if @pos >= @bytes.bytesize
133
+
134
+ initial_byte = @bytes[@pos].ord
135
+ @pos += 1
136
+
137
+ major_type = initial_byte >> 5
138
+ additional = initial_byte & 0x1F
139
+
140
+ value = if major_type == MAJOR_TYPE_SIMPLE
141
+ additional
142
+ else
143
+ decode_additional_info(additional)
144
+ end
145
+
146
+ [major_type, value]
147
+ end
148
+
149
+ # Decode additional information (RFC 8949 §3)
150
+ #
151
+ # Additional info encoding:
152
+ # 0-23: Value is directly in additional info
153
+ # 24: 1-byte uint8 follows
154
+ # 25: 2-byte uint16 follows
155
+ # 26: 4-byte uint32 follows
156
+ # 27: 8-byte uint64 follows
157
+ # 28-30: Reserved (error)
158
+ # 31: Indefinite length (not supported in minimal impl)
159
+ def decode_additional_info(additional)
160
+ case additional
161
+ when 0..23
162
+ # Value directly encoded
163
+ additional
164
+ when 24
165
+ # 1-byte uint8 follows
166
+ read_uint8
167
+ when 25
168
+ # 2-byte uint16 follows
169
+ read_uint16
170
+ when 26
171
+ # 4-byte uint32 follows
172
+ read_uint32
173
+ when 27
174
+ # 8-byte uint64 follows
175
+ read_uint64
176
+ when 28, 29, 30
177
+ raise DecodeError, "Reserved additional info value: #{additional}"
178
+ when 31
179
+ raise UnsupportedError, 'Indefinite-length items not supported in minimal implementation'
180
+ end
181
+ end
182
+
183
+ # Read unsigned 8-bit integer
184
+ def read_uint8
185
+ check_available(1)
186
+ val = @bytes[@pos].ord
187
+ @pos += 1
188
+ val
189
+ end
190
+
191
+ # Read unsigned 16-bit integer (big-endian)
192
+ def read_uint16
193
+ check_available(2)
194
+ val = @bytes[@pos, 2].unpack1('n')
195
+ @pos += 2
196
+ val
197
+ end
198
+
199
+ # Read unsigned 32-bit integer (big-endian)
200
+ def read_uint32
201
+ check_available(4)
202
+ val = @bytes[@pos, 4].unpack1('N')
203
+ @pos += 4
204
+ val
205
+ end
206
+
207
+ # Read unsigned 64-bit integer (big-endian)
208
+ def read_uint64
209
+ check_available(8)
210
+ val = @bytes[@pos, 8].unpack1('Q>')
211
+ @pos += 8
212
+ val
213
+ end
214
+
215
+ # Read byte string (binary data)
216
+ def read_bytes(length)
217
+ check_collection_size(length)
218
+ check_available(length)
219
+
220
+ bytes = @bytes[@pos, length]
221
+ @pos += length
222
+ bytes
223
+ end
224
+
225
+ # Read UTF-8 text string
226
+ def read_string(length)
227
+ check_collection_size(length)
228
+ check_available(length)
229
+
230
+ str = @bytes[@pos, length]
231
+ @pos += length
232
+
233
+ # Force UTF-8 encoding and validate
234
+ str.force_encoding('UTF-8')
235
+
236
+ raise DecodeError, 'Invalid UTF-8 encoding in text string' unless str.valid_encoding?
237
+
238
+ str
239
+ end
240
+
241
+ # Read array
242
+ def read_array(length)
243
+ check_collection_size(length)
244
+
245
+ @depth += 1
246
+
247
+ arr = Array.new(length) { decode }
248
+
249
+ @depth -= 1
250
+ arr
251
+ end
252
+
253
+ # Read map (hash)
254
+ def read_map(length)
255
+ check_collection_size(length)
256
+
257
+ @depth += 1
258
+
259
+ hash = {}
260
+
261
+ length.times do
262
+ key = decode
263
+ value = decode
264
+
265
+ # Convert symbol keys to strings for consistency
266
+ key = key.to_s if key.is_a?(Symbol)
267
+
268
+ hash[key] = value
269
+ end
270
+
271
+ @depth -= 1
272
+ hash
273
+ end
274
+
275
+ # Read tagged value (RFC 8949 §3.4)
276
+ def read_tagged(tag)
277
+ case tag
278
+ when TAG_EPOCH_TIMESTAMP
279
+ # Tag 1: Epoch-based timestamp
280
+ timestamp_value = decode
281
+
282
+ case timestamp_value
283
+ when Integer, Float
284
+ Time.at(timestamp_value)
285
+ else
286
+ raise DecodeError, "Invalid timestamp value type: #{timestamp_value.class}"
287
+ end
288
+ else
289
+ # Unknown tag: decode value but ignore tag
290
+ # This allows forward compatibility
291
+ decode
292
+ end
293
+ end
294
+
295
+ # Read simple value (RFC 8949 §3.3)
296
+ def read_simple(value)
297
+ case value
298
+ when SIMPLE_FALSE
299
+ false
300
+ when SIMPLE_TRUE
301
+ true
302
+ when SIMPLE_NULL
303
+ nil
304
+ when SIMPLE_FLOAT16
305
+ # 16-bit float (half-precision)
306
+ read_float16
307
+ when SIMPLE_FLOAT32
308
+ # 32-bit float (single-precision)
309
+ read_float32
310
+ when SIMPLE_FLOAT64
311
+ # 64-bit float (double-precision)
312
+ read_float64
313
+ when 0..19
314
+ # Unassigned simple values (0-19)
315
+ raise UnsupportedError, "Unassigned simple value: #{value}"
316
+ when 24..31
317
+ # Should not reach here (handled in decode_additional_info)
318
+ raise DecodeError, "Invalid simple value: #{value}"
319
+ else
320
+ # Simple values 32-255 (extended)
321
+ raise UnsupportedError, "Extended simple values not supported: #{value}"
322
+ end
323
+ end
324
+
325
+ # Read 16-bit float (IEEE 754 half-precision)
326
+ def read_float16
327
+ check_available(2)
328
+
329
+ # Read 16-bit big-endian
330
+ half = @bytes[@pos, 2].unpack1('n')
331
+ @pos += 2
332
+
333
+ # Convert IEEE 754 half to Ruby float
334
+ # Format: 1 sign bit, 5 exponent bits, 10 mantissa bits
335
+ sign = (half >> 15) & 0x1
336
+ exponent = (half >> 10) & 0x1F
337
+ mantissa = half & 0x3FF
338
+
339
+ if exponent.zero?
340
+ # Subnormal or zero
341
+ result = mantissa.to_f / (2**24)
342
+ elsif exponent == 0x1F
343
+ # Infinity or NaN
344
+ return mantissa.zero? ? Float::INFINITY : Float::NAN
345
+ else
346
+ # Normalized
347
+ result = (1.0 + (mantissa.to_f / (2**10))) * (2**(exponent - 15))
348
+ end
349
+
350
+ sign.zero? ? result : -result
351
+ end
352
+
353
+ # Read 32-bit float (IEEE 754 single-precision)
354
+ def read_float32
355
+ check_available(4)
356
+ float_bytes = @bytes[@pos, 4]
357
+ @pos += 4
358
+ float_bytes.unpack1('g') # Big-endian single-precision float
359
+ end
360
+
361
+ # Read 64-bit float (IEEE 754 double-precision)
362
+ def read_float64
363
+ check_available(8)
364
+ float_bytes = @bytes[@pos, 8]
365
+ @pos += 8
366
+ float_bytes.unpack1('G') # Big-endian double-precision float
367
+ end
368
+
369
+ # Check if enough bytes are available
370
+ def check_available(needed)
371
+ available = @bytes.bytesize - @pos
372
+ return if available >= needed
373
+
374
+ raise DecodeError, "Unexpected end of input (need #{needed} bytes, have #{available})"
375
+ end
376
+
377
+ # Check collection size to prevent memory exhaustion
378
+ def check_collection_size(size)
379
+ return if size <= MAX_COLLECTION_SIZE
380
+
381
+ raise DecodeError, "Collection size #{size} exceeds maximum (#{MAX_COLLECTION_SIZE})"
382
+ end
383
+ end # rubocop:enable Metrics/ClassLength
384
+ end
385
+ end