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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'observable/reactor'
4
+
5
+ module Takagi
6
+ # Backward compatibility alias for Observable::Reactor
7
+ #
8
+ # @deprecated Use {Observable::Reactor} instead. This alias will be removed in v2.0.0
9
+ #
10
+ # The new Observable::Reactor provides:
11
+ # - Thread pool support for parallel execution
12
+ # - Event-driven notifications via EventBus
13
+ # - Hybrid polling + push modes
14
+ # - Better resource management
15
+ #
16
+ # @example Migration
17
+ # # Old
18
+ # reactor = Takagi::Reactor.new
19
+ #
20
+ # # New
21
+ # reactor = Takagi::Observable::Reactor.new(threads: 4)
22
+ Reactor = Observable::Reactor
23
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ # Tracks background reactor instances and controls their lifecycle.
5
+ #
6
+ # Uses Registry::Base for thread-safe storage and consistent API.
7
+ # Reactors are stored with auto-incrementing IDs.
8
+ #
9
+ # NOTE: Cannot be moved to Reactor::Registry because Reactor is already a class.
10
+ # Keeping as ReactorRegistry at top level for now.
11
+ #
12
+ # @example Register and manage reactors
13
+ # reactor = SomeReactor.new
14
+ # id = ReactorRegistry.register(reactor)
15
+ # ReactorRegistry.start_all
16
+ # ReactorRegistry.stop_all
17
+ module ReactorRegistry
18
+ extend Registry::Base
19
+
20
+ class << self
21
+ # Register a reactor instance
22
+ #
23
+ # @param reactor [Object] Reactor instance
24
+ # @return [Integer] Auto-assigned reactor ID
25
+ def register(reactor)
26
+ id = next_id
27
+ super(id, reactor)
28
+ id
29
+ end
30
+
31
+ # Get all registered reactors
32
+ #
33
+ # @return [Array] List of reactor instances
34
+ def reactors
35
+ @mutex.synchronize { registry.values }
36
+ end
37
+
38
+ # Start all registered reactors
39
+ #
40
+ # @return [void]
41
+ def start_all
42
+ reactors.each(&:start)
43
+ end
44
+
45
+ # Stop all registered reactors
46
+ #
47
+ # @return [void]
48
+ def stop_all
49
+ reactors.each do |reactor|
50
+ reactor.stop if reactor.respond_to?(:stop)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def next_id
57
+ @mutex.synchronize do
58
+ @next_id ||= 0
59
+ @next_id += 1
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ module Registry
5
+ # Thread-safe base module for implementing registries.
6
+ #
7
+ # Provides a consistent API and thread-safety for all registry implementations.
8
+ # Designed to support the plugin system by allowing safe concurrent access.
9
+ #
10
+ # @example Basic usage with class methods
11
+ # class MyRegistry
12
+ # extend Takagi::Registry::Base
13
+ # end
14
+ #
15
+ # MyRegistry.register(:foo, SomeClass)
16
+ # MyRegistry.get(:foo) # => SomeClass
17
+ # MyRegistry.registered?(:foo) # => true
18
+ #
19
+ # @example With validation
20
+ # class MyRegistry
21
+ # extend Takagi::Registry::Base
22
+ #
23
+ # def self.validate_entry!(key, value, **metadata)
24
+ # raise ArgumentError, "Invalid value" unless value.respond_to?(:call)
25
+ # end
26
+ # end
27
+ #
28
+ # @example With custom storage structure
29
+ # class MyRegistry
30
+ # extend Takagi::Registry::Base
31
+ #
32
+ # def self.store_entry(key, value, **metadata)
33
+ # registry[key] = { klass: value, options: metadata }
34
+ # end
35
+ # end
36
+ module Base
37
+ # Error raised when a registry entry is not found
38
+ class NotFoundError < StandardError; end
39
+
40
+ # Error raised when a registry entry already exists
41
+ class AlreadyRegisteredError < StandardError; end
42
+
43
+ # Error raised when validation fails
44
+ class ValidationError < StandardError; end
45
+
46
+ def self.extended(base)
47
+ base.instance_variable_set(:@registry, {})
48
+ base.instance_variable_set(:@mutex, Mutex.new)
49
+ end
50
+
51
+ # Register a new entry in the registry
52
+ #
53
+ # @param key [Object] Unique identifier for the entry
54
+ # @param value [Object] The value to register
55
+ # @param metadata [Hash] Optional metadata to store with the entry
56
+ # @param overwrite [Boolean] Whether to allow overwriting existing entries
57
+ # @return [void]
58
+ # @raise [AlreadyRegisteredError] If key already exists and overwrite is false
59
+ # @raise [ValidationError] If validation fails
60
+ #
61
+ # @example
62
+ # MyRegistry.register(:udp, UdpTransport, rfc: 'RFC 7252')
63
+ def register(key, value, overwrite: false, **metadata)
64
+ validate_entry!(key, value, **metadata) if respond_to?(:validate_entry!, true)
65
+
66
+ @mutex.synchronize do
67
+ if registry.key?(key) && !overwrite
68
+ # Enhanced error with suggestions
69
+ error = Errors::RegistryError.already_registered(self.class.name, key)
70
+ raise AlreadyRegisteredError, error.message
71
+ end
72
+
73
+ store_entry(key, value, **metadata)
74
+ end
75
+
76
+ after_register(key, value, **metadata) if respond_to?(:after_register, true)
77
+ end
78
+
79
+ # Retrieve an entry from the registry
80
+ #
81
+ # @param key [Object] The key to look up
82
+ # @return [Object] The registered value
83
+ # @raise [NotFoundError] If key is not found
84
+ #
85
+ # @example
86
+ # transport = MyRegistry.get(:udp)
87
+ def get(key)
88
+ value = self[key]
89
+ unless value
90
+ # Enhanced error with suggestions
91
+ available = keys
92
+ error = Errors::RegistryError.not_found(
93
+ self.class.name,
94
+ key,
95
+ available
96
+ )
97
+ raise NotFoundError, error.message
98
+ end
99
+
100
+ value
101
+ end
102
+
103
+ # Retrieve an entry from the registry (returns nil if not found)
104
+ #
105
+ # @param key [Object] The key to look up
106
+ # @return [Object, nil] The registered value or nil
107
+ #
108
+ # @example
109
+ # transport = MyRegistry[:udp]
110
+ def [](key)
111
+ @mutex.synchronize { fetch_entry(key) }
112
+ end
113
+
114
+ # Check if a key is registered
115
+ #
116
+ # @param key [Object] The key to check
117
+ # @return [Boolean] true if registered
118
+ #
119
+ # @example
120
+ # MyRegistry.registered?(:udp) # => true
121
+ def registered?(key)
122
+ @mutex.synchronize { registry.key?(key) }
123
+ end
124
+
125
+ # Get all registered keys
126
+ #
127
+ # @return [Array] List of all keys
128
+ #
129
+ # @example
130
+ # MyRegistry.keys # => [:udp, :tcp, :dtls]
131
+ def keys
132
+ @mutex.synchronize { registry.keys.dup }
133
+ end
134
+ alias all keys
135
+
136
+ # Get all entries as a hash
137
+ #
138
+ # @return [Hash] Copy of the registry
139
+ #
140
+ # @example
141
+ # MyRegistry.entries # => { udp: UdpTransport, tcp: TcpTransport }
142
+ def entries
143
+ @mutex.synchronize { registry.dup }
144
+ end
145
+
146
+ # Get metadata for a registered entry
147
+ #
148
+ # @param key [Object] The key to look up
149
+ # @return [Hash, nil] Metadata hash or nil if not found
150
+ #
151
+ # @example
152
+ # MyRegistry.metadata_for(:udp) # => { rfc: 'RFC 7252' }
153
+ def metadata_for(key)
154
+ @mutex.synchronize { fetch_metadata(key) }
155
+ end
156
+
157
+ # Unregister an entry
158
+ #
159
+ # @param key [Object] The key to remove
160
+ # @return [Object, nil] The removed value or nil if not found
161
+ #
162
+ # @example
163
+ # MyRegistry.unregister(:udp)
164
+ def unregister(key)
165
+ @mutex.synchronize do
166
+ before_unregister(key) if respond_to?(:before_unregister, true)
167
+ registry.delete(key)
168
+ end
169
+ end
170
+
171
+ # Clear all registrations
172
+ #
173
+ # Primarily useful for testing. Thread-safe.
174
+ #
175
+ # @return [void]
176
+ #
177
+ # @example
178
+ # MyRegistry.clear!
179
+ def clear!
180
+ @mutex.synchronize do
181
+ before_clear if respond_to?(:before_clear, true)
182
+ registry.clear
183
+ end
184
+ end
185
+
186
+ # Get count of registered entries
187
+ #
188
+ # @return [Integer] Number of registered entries
189
+ #
190
+ # @example
191
+ # MyRegistry.count # => 3
192
+ def count
193
+ @mutex.synchronize { registry.size }
194
+ end
195
+ alias size count
196
+
197
+ # Check if registry is empty
198
+ #
199
+ # @return [Boolean] true if no entries registered
200
+ #
201
+ # @example
202
+ # MyRegistry.empty? # => false
203
+ def empty?
204
+ @mutex.synchronize { registry.empty? }
205
+ end
206
+
207
+ # Iterate over all entries
208
+ #
209
+ # @yield [key, value] Each key-value pair
210
+ # @return [Enumerator] If no block given
211
+ #
212
+ # @example
213
+ # MyRegistry.each do |key, value|
214
+ # puts "#{key}: #{value}"
215
+ # end
216
+ def each(&block)
217
+ return enum_for(:each) unless block_given?
218
+
219
+ # Get snapshot to avoid holding lock during iteration
220
+ snapshot = @mutex.synchronize { registry.dup }
221
+ snapshot.each(&block)
222
+ end
223
+
224
+ private
225
+
226
+ # Access to the internal registry hash
227
+ # Must be called within mutex.synchronize block
228
+ attr_reader :registry
229
+
230
+ # Store an entry in the registry (can be overridden)
231
+ #
232
+ # @param key [Object] The key
233
+ # @param value [Object] The value
234
+ # @param metadata [Hash] Metadata
235
+ def store_entry(key, value, **metadata)
236
+ if metadata.empty?
237
+ registry[key] = value
238
+ else
239
+ registry[key] = { value: value, metadata: metadata }
240
+ end
241
+ end
242
+
243
+ # Fetch an entry from the registry (can be overridden)
244
+ #
245
+ # @param key [Object] The key
246
+ # @return [Object, nil] The value or nil
247
+ def fetch_entry(key)
248
+ entry = registry[key]
249
+ return nil unless entry
250
+
251
+ # Handle both simple values and hash-wrapped values
252
+ entry.is_a?(Hash) && entry.key?(:value) ? entry[:value] : entry
253
+ end
254
+
255
+ # Fetch metadata for an entry (can be overridden)
256
+ #
257
+ # @param key [Object] The key
258
+ # @return [Hash, nil] The metadata or nil
259
+ def fetch_metadata(key)
260
+ entry = registry[key]
261
+ return nil unless entry
262
+ return nil unless entry.is_a?(Hash)
263
+
264
+ entry[:metadata]
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'hooks'
4
+ require_relative 'serialization'
5
+
6
+ module Takagi
7
+ # Builds CoAP responses from middleware results
8
+ class ResponseBuilder
9
+ DEFAULT_RESPONSE_CODE = CoAP::Registries::Response::CONTENT
10
+
11
+ # Builds a response from the middleware result
12
+ #
13
+ # @param inbound_request [Takagi::Message::Inbound] The original request
14
+ # @param result [Takagi::Message::Outbound, Hash, Object] The middleware result
15
+ # @param logger [Logger, nil] Optional logger for debugging
16
+ # @return [Takagi::Message::Outbound] The response message
17
+ def self.build(inbound_request, result, logger: nil)
18
+ Takagi::Hooks.emit(:before_response_build, inbound: inbound_request, result: result)
19
+
20
+ case result
21
+ when Takagi::Message::Outbound
22
+ response = result
23
+ when Hash, String
24
+ logger&.debug("Returned #{result.class} as response")
25
+ response = respond(inbound_request, result, logger: logger)
26
+ else
27
+ logger&.warn("Middleware returned non-Hash: #{result.inspect}")
28
+ response = inbound_request.to_response('5.00 Internal Server Error', { error: 'Internal Server Error' })
29
+ end
30
+
31
+ Takagi::Hooks.emit(:after_response_build, inbound: inbound_request, response: response, result: result)
32
+ response
33
+ end
34
+
35
+ # Build a CoAP response with content-format negotiation.
36
+ #
37
+ # @param inbound_request [Takagi::Message::Inbound] The original request
38
+ # @param payload [Object] The payload to serialize
39
+ # @param code [Integer, String, Symbol] CoAP response code (default 2.05 Content)
40
+ # @param formats [Array<Integer, Symbol, String>, nil] Allowed content-format codes
41
+ # @param force [Integer, Symbol, String, nil] Force a specific content-format code
42
+ # @param options [Hash] Additional CoAP options
43
+ # @param logger [Logger, nil] Logger for debug output
44
+ # @return [Takagi::Message::Outbound]
45
+ def self.respond(inbound_request, payload, code: DEFAULT_RESPONSE_CODE, formats: nil, force: nil, options: {}, logger: nil)
46
+ requested_formats = formats || inbound_request&.content_format
47
+ allowed_formats = normalize_formats(requested_formats)
48
+ forced_format = normalize_format(force)
49
+ accept_format = normalize_format(extract_accept(inbound_request))
50
+
51
+ selected_format = select_format(allowed_formats, accept_format, forced_format, logger)
52
+
53
+ case selected_format
54
+ when :not_acceptable
55
+ return inbound_request.to_response(CoAP::Registries::Response::NOT_ACCEPTABLE, { error: 'Not Acceptable' })
56
+ when :unsupported
57
+ return inbound_request.to_response(CoAP::Registries::Response::UNSUPPORTED_CONTENT_FORMAT, { error: 'Unsupported Content-Format' })
58
+ end
59
+
60
+ content_options = (options || {}).dup
61
+ if selected_format
62
+ content_options[CoAP::Registries::Option::CONTENT_FORMAT] ||= [selected_format]
63
+ end
64
+
65
+ inbound_request.to_response(code, payload, options: content_options)
66
+ end
67
+
68
+ class << self
69
+ private
70
+
71
+ def normalize_formats(formats)
72
+ list = Array(formats || default_format)
73
+ normalized = list.map { |fmt| normalize_format(fmt) }.compact
74
+ normalized.empty? ? [default_format] : normalized
75
+ end
76
+
77
+ def select_format(allowed_formats, accept_format, forced_format, logger)
78
+ if forced_format
79
+ return :unsupported unless Serialization::Registry.supports?(forced_format)
80
+
81
+ return forced_format
82
+ end
83
+
84
+ if accept_format
85
+ if Serialization::Registry.supports?(accept_format) && allowed_formats.include?(accept_format)
86
+ return accept_format
87
+ end
88
+
89
+ logger&.debug("Accept format #{accept_format.inspect} not supported")
90
+ return :not_acceptable
91
+ end
92
+
93
+ allowed_formats.find { |fmt| Serialization::Registry.supports?(fmt) } || default_format
94
+ end
95
+
96
+ def normalize_format(format)
97
+ return nil if format.nil?
98
+ return format if format.is_a?(Integer)
99
+ return format.content_format_code if format.respond_to?(:content_format_code)
100
+
101
+ if format.is_a?(Array)
102
+ return normalize_format(format.first)
103
+ end
104
+
105
+ if format.is_a?(Symbol) || format.is_a?(String)
106
+ registry_value = CoAP::Registries::ContentFormat.value_for(format) ||
107
+ CoAP::Registries::ContentFormat.value_for(format.to_s.downcase.to_sym)
108
+ return registry_value if registry_value
109
+ end
110
+
111
+ if format.is_a?(String)
112
+ decoded = decode_content_format_bytes(format)
113
+ return decoded if decoded
114
+ end
115
+
116
+ nil
117
+ end
118
+
119
+ def decode_content_format_bytes(value)
120
+ bytes = value.b.bytes
121
+ return nil if bytes.empty? || bytes.length > 2
122
+
123
+ bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
124
+ end
125
+
126
+ def extract_accept(inbound_request)
127
+ return unless inbound_request.respond_to?(:accept)
128
+
129
+ inbound_request.accept
130
+ end
131
+
132
+ def default_format
133
+ if defined?(Takagi::Router::DEFAULT_CONTENT_FORMAT)
134
+ Takagi::Router::DEFAULT_CONTENT_FORMAT
135
+ else
136
+ CoAP::Registries::ContentFormat::JSON
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Takagi
6
+ class Router
7
+ # Handles metadata extraction from route handlers at boot time.
8
+ #
9
+ # Extracted from Router to follow Single Responsibility Principle.
10
+ # Executes route handlers in a special context to capture CoRE Link Format
11
+ # metadata defined via core blocks.
12
+ class MetadataExtractor
13
+ # @param logger [Logger] Logger instance for debugging
14
+ def initialize(logger)
15
+ @logger = logger
16
+ end
17
+
18
+ # Executes route handler in metadata extraction mode to capture core block attributes
19
+ # This allows defining metadata inline with the handler for better DX
20
+ #
21
+ # @param entry [RouteEntry] The route entry to extract metadata from
22
+ def extract(entry)
23
+ # Skip metadata extraction for discovery routes to avoid deadlock
24
+ # Discovery routes access the router itself, which would cause a deadlock
25
+ # since we're already holding the routes_mutex. These routes declare
26
+ # their metadata explicitly via the metadata: parameter instead.
27
+ if entry.metadata[:discovery]
28
+ @logger.debug "Skipping metadata extraction for discovery route: #{entry.method} #{entry.path}"
29
+ return
30
+ end
31
+
32
+ # Create a mock request object that will be passed to the handler
33
+ mock_request = MetadataExtractionRequest.new
34
+
35
+ # Create a special extraction context that uses the entry's AttributeSet directly
36
+ # (This is safe because it runs once at boot time, not during concurrent requests)
37
+ context = MetadataExtractionContext.new(entry, mock_request, {}, entry.receiver)
38
+
39
+ # Execute the handler block - it may call core { ... } which updates the attribute_set
40
+ begin
41
+ context.run(entry.block)
42
+ rescue StandardError => e
43
+ # If the handler fails during metadata extraction (e.g., tries to access real data),
44
+ # that's okay - we only care about core blocks which should not throw errors
45
+ @logger.debug "Metadata extraction for #{entry.method} #{entry.path} encountered: #{e.message}"
46
+ end
47
+
48
+ # Apply any changes made by core blocks
49
+ entry.attribute_set.apply!
50
+ end
51
+
52
+ # Special context for boot-time metadata extraction
53
+ # Uses entry's AttributeSet directly (safe because boot-time is single-threaded)
54
+ #
55
+ # Note: This class inherits from Takagi::Router::RouteContext, but since this file
56
+ # is loaded before RouteContext is defined, we define it as a placeholder here
57
+ # and will reopen it after Router is loaded.
58
+ class MetadataExtractionContext
59
+ include Takagi::Helpers
60
+ extend Forwardable
61
+
62
+ attr_reader :request, :params
63
+
64
+ # Delegate CoRE attribute methods to @core_attributes
65
+ def_delegators :@core_attributes, :core, :metadata, :attribute
66
+ def_delegators :@core_attributes, :ct, :sz, :title, :obs, :rt, :interface
67
+
68
+ def initialize(entry, request, params, receiver)
69
+ @entry = entry
70
+ @request = request
71
+ @params = params
72
+ @receiver = receiver
73
+ # Use entry's AttributeSet directly for boot-time extraction
74
+ # This is safe because metadata extraction runs once at boot time (single-threaded)
75
+ @core_attributes = @entry.attribute_set
76
+ end
77
+
78
+ def run(block)
79
+ return unless block
80
+
81
+ args = case block.arity
82
+ when 0 then []
83
+ when 1 then [request]
84
+ else
85
+ [request, params]
86
+ end
87
+ args = [request, params] if block.arity.negative?
88
+
89
+ # Support halt for early returns
90
+ result = catch(:halt) do
91
+ instance_exec(*args, &block)
92
+ end
93
+
94
+ result
95
+ ensure
96
+ @core_attributes.apply!
97
+ end
98
+
99
+ private
100
+
101
+ # Delegates method calls to the receiver (application instance)
102
+ def method_missing(name, ...)
103
+ if @receiver.respond_to?(name)
104
+ @receiver.public_send(name, ...)
105
+ else
106
+ super
107
+ end
108
+ end
109
+
110
+ # Required pair for method_missing
111
+ def respond_to_missing?(name, include_private = false)
112
+ @receiver.respond_to?(name, include_private) || super
113
+ end
114
+ end
115
+
116
+ # Mock request object used during metadata extraction
117
+ # Provides minimal interface to prevent errors when handlers are executed at boot time
118
+ class MetadataExtractionRequest
119
+ def to_response(*_args)
120
+ nil # Ignore response generation during metadata extraction
121
+ end
122
+
123
+ def method_missing(_name, *_args, &_block)
124
+ nil # Return nil for any method calls to prevent errors
125
+ end
126
+
127
+ def respond_to_missing?(_name, _include_private = false)
128
+ true # Pretend to respond to everything
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end