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/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Takagi – Lightweight CoAP Framework for Ruby
2
2
 
3
- <!---
4
3
  [![Gem Version](https://badge.fury.io/rb/takagi.svg)](https://rubygems.org/gems/takagi)
5
- -->
6
4
  [![Build Status](https://github.com/domitea/takagi/actions/workflows/main.yml/badge.svg)](https://github.com/domitea/takagi/actions)
7
5
 
8
6
  ## About Takagi
@@ -11,8 +9,26 @@
11
9
  It provides a lightweight way to build **CoAP APIs**, handle **IoT messaging**, and process sensor data efficiently.
12
10
 
13
11
  🔹 **Minimalistic DSL** – Define CoAP endpoints just like in Sinatra.
12
+ 🔹 **CoRE discovery helpers** – Configure link-format metadata inline or at boot.
14
13
  🔹 **Efficient and fast** – Runs over UDP, ideal for IoT applications.
15
- 🔹 **Database-ready** – Seamless integration with **Sequel** for storing device data.
14
+ 🔹 **Reliable transport** – Supports CoAP over TCP (RFC 8323).
15
+
16
+ ## Why "Takagi"?
17
+ The name **Takagi** is inspired by **Riyoko Takagi**, as a nod to the naming convention of Sinatra.
18
+ Just like Sinatra simplified web applications in Ruby, Takagi aims to simplify CoAP-based IoT communication in Ruby. It embodies minimalism, efficiency, and a straightforward approach to handling CoAP requests.
19
+ Additionally, both Sinatra and Takagi share a connection to **jazz music**, as Riyoko Takagi is a known jazz pianist, much like how Sinatra was named after the legendary Frank Sinatra.
20
+
21
+ ---
22
+
23
+ ## Philosophy
24
+
25
+ **Takagi treats CoAP as a first-class HTTP-equivalent protocol.**
26
+
27
+ CoAP (RFC 7252) was explicitly designed to mirror HTTP semantics for constrained environments. Yet most CoAP libraries (libcoap, aiocoap, californium) force developers into unfamiliar programming patterns that ignore decades of HTTP best practices.
28
+
29
+ Takagi rejects this approach. If CoAP is "HTTP for IoT," then CoAP servers should feel like HTTP servers. Routes, middleware, REST semantics—these patterns work. Why abandon them?
30
+
31
+ **Takagi brings Sinatra's elegance to CoAP.** If you can build a web API, you can build an IoT API.
16
32
 
17
33
  ---
18
34
 
@@ -35,28 +51,458 @@ gem install takagi
35
51
  ## Getting Started
36
52
 
37
53
  ### **Create a new Takagi API**
54
+
55
+ Takagi provides a **Sinatra-like DSL** with modern Ruby conveniences:
56
+
38
57
  ```ruby
39
58
  require 'takagi'
40
59
 
41
60
  class SensorAPI < Takagi::Base
42
- get "/sensor/:id" do |params|
43
- Sensor[params[:id].to_i].to_json
61
+ # Simple GET with auto-extracted params
62
+ get '/sensor/:id' do
63
+ json id: params[:id], value: 22.5, status: 'active'
44
64
  end
45
65
 
46
- post "/sensor" do |params|
47
- Sensor.create(params).to_json
66
+ # POST with validation helpers
67
+ post '/sensor' do
68
+ validate_params :value # Raises if missing
69
+
70
+ created sensor_id: rand(1000), value: params[:value]
71
+ end
72
+
73
+ # Early returns with halt
74
+ get '/restricted' do
75
+ halt forbidden('Access denied') unless authorized?
76
+ json message: 'Welcome'
77
+ end
78
+
79
+ # Delete with status helpers
80
+ delete '/sensor/:id' do
81
+ deleted message: "Sensor #{params[:id]} removed"
48
82
  end
49
83
  end
50
84
 
51
- Takagi.run!
85
+ SensorAPI.run!
86
+ # To serve CoAP over TCP instead of UDP:
87
+ # SensorAPI.run!(protocols: [:tcp])
88
+ # To serve both TCP and UDP simultaneously:
89
+ # SensorAPI.run!(protocols: [:udp, :tcp])
52
90
  ```
53
91
  🔥 **Boom! You just built a CoAP API in Ruby.**
54
92
 
93
+ ### **Configure CoRE discovery metadata**
94
+
95
+ Expose meaningful attributes in `/.well-known/core` either inline or after registration:
96
+
97
+ ```ruby
98
+ class SensorAPI < Takagi::Base
99
+ get '/metrics' do
100
+ core do
101
+ title 'Environment Metrics'
102
+ rt %w[sensor.metrics sensor.temp]
103
+ interface 'core.s'
104
+ sz 256 # expected size of data
105
+ end
106
+
107
+ { temp: 21.4, humidity: 0.48 }
108
+ end
109
+ end
110
+
111
+ # Configure discovery data separately (e.g. in an initializer):
112
+ SensorAPI.core '/metrics' do
113
+ title 'Environment Metrics'
114
+ ct 'application/cbor'
115
+ end
116
+ ```
117
+
118
+ Both approaches share the same DSL, so you can keep route handlers focused while still
119
+ publishing rich metadata.
120
+
121
+ ### **But CoAP is not only GET, POST, PUT and DELETE. There is also Observe!**
122
+ ```ruby
123
+ require 'takagi'
124
+
125
+ class SensorAPI < Takagi::Base
126
+ reactor do
127
+ # observable: Server-side - make a resource observable by clients
128
+ observable "/sensors/temp" do
129
+ core do
130
+ title 'Temperature Stream'
131
+ rt 'sensor.temp.streaming'
132
+ end
133
+
134
+ json temp: 42.0, unit: 'celsius'
135
+ end
136
+
137
+ # observe: Client-side - subscribe to remote observable resources
138
+ observe "coap://temp_server/temp" do |request, params|
139
+ Takagi.logger.info "Remote temperature update: #{params.inspect}"
140
+ end
141
+ end
142
+ end
143
+
144
+ SensorAPI.run!
145
+ ```
146
+ 🔥 **Takagi is Observe (RFC 7641) enabled**
147
+
148
+ **Important distinction:**
149
+ - **`observable`** - Server-side: Makes your resource observable by clients
150
+ - **`observe`** - Client-side: Subscribe to remote observable resources
151
+
152
+ ---
153
+
154
+ ## Other features
155
+
156
+ Takagi provides a rich set of helper methods to make your code cleaner and more intuitive.
157
+ Why? Because even if CoAP is like HTTP, then you should make new endpoints with ease.
158
+
159
+ ### **Response Helpers**
160
+
161
+ Instead of manually constructing responses, use semantic helpers:
162
+
163
+ ```ruby
164
+ class MyAPI < Takagi::Base
165
+ get '/users' do
166
+ json users: User.all # Returns 2.05 Content
167
+ end
168
+
169
+ post '/users' do
170
+ validate_params :name, :email
171
+ created user_id: 123, name: params[:name] # Returns 2.01 Created
172
+ end
173
+
174
+ put '/users/:id' do
175
+ changed message: 'User updated' # Returns 2.04 Changed
176
+ end
177
+
178
+ delete '/users/:id' do
179
+ deleted message: 'User removed' # Returns 2.02 Deleted
180
+ end
181
+ end
182
+ ```
183
+
184
+ **Available response helpers:**
185
+ - `json(data)` - 2.05 Content
186
+ - `created(data)` - 2.01 Created
187
+ - `changed(data)` - 2.04 Changed
188
+ - `deleted(data)` - 2.02 Deleted
189
+ - `valid(data)` - 2.03 Valid
190
+ - `respond(payload, code: 2.05, formats: [ct], force: nil)` - negotiate `Content-Format` via `Accept`/defaults and build the CoAP response for you. `formats` is the allowed list; `force` pins a format (e.g., SenML CBOR) and returns `4.15` if unsupported. Unsupported `Accept` yields `4.06`.
191
+ - If you declare `ct` in your CoRE metadata, `respond` will use that as the allowed list automatically, so discovery and runtime stay in sync.
192
+
193
+ Quick examples:
194
+
195
+ ```ruby
196
+ get '/sensor' do
197
+ # Default (router content-format, typically JSON)
198
+ respond(sensor: 22.5)
199
+ end
200
+
201
+ get '/sensor' do
202
+ # Allow JSON or CBOR, honor Accept
203
+ respond({ sensor: 22.5 }, formats: [
204
+ CoAP::Registries::ContentFormat::JSON,
205
+ CoAP::Registries::ContentFormat::CBOR
206
+ ])
207
+ end
208
+
209
+ get '/senml' do
210
+ # Force a required format (e.g., SenML CBOR)
211
+ respond(senml_payload, force: CoAP::Registries::ContentFormat::CBOR)
212
+ end
213
+ ```
214
+
215
+ ### **Error Response Helpers**
216
+
217
+ Handle errors elegantly with built-in helpers:
218
+
219
+ ```ruby
220
+ class MyAPI < Takagi::Base
221
+ get '/resource/:id' do
222
+ halt not_found("Resource #{params[:id]} not found") unless exists?(params[:id])
223
+
224
+ json data: fetch_resource(params[:id])
225
+ end
226
+
227
+ post '/restricted' do
228
+ halt unauthorized('Please authenticate') unless authenticated?
229
+ halt forbidden('Insufficient permissions') unless authorized?
230
+
231
+ created message: 'Success'
232
+ end
233
+
234
+ get '/validate' do
235
+ halt bad_request('Invalid input') unless valid_input?
236
+
237
+ json status: 'ok'
238
+ end
239
+ end
240
+ ```
241
+
242
+ **Available error helpers:**
243
+ - `bad_request(msg)` - 4.00 Bad Request
244
+ - `unauthorized(msg)` - 4.01 Unauthorized
245
+ - `forbidden(msg)` - 4.03 Forbidden
246
+ - `not_found(msg)` - 4.04 Not Found
247
+ - `method_not_allowed(msg)` - 4.05 Method Not Allowed
248
+ - `server_error(msg)` - 5.00 Internal Server Error
249
+ - `service_unavailable(msg)` - 5.03 Service Unavailable
250
+
251
+ ### **Plugins**
252
+
253
+ Ship features as plugins under `Takagi::Plugins::<Name>` with `.apply(app, opts = {})`. Plugins can:
254
+ - Add routes/controllers (optionally auto-prefixed via `route_prefix` metadata)
255
+ - Register serializers/content-formats
256
+ - Register new transports/servers
257
+ - Extend CoAP registries (methods, responses, options, content-formats)
258
+ - Hook into lifecycle events (`docs/FIRST_PLUGIN_GUIDE.md`, `docs/HOOKS.md`)
259
+
260
+ Enable with `plugin :name, order: 0, **options` in your app and call `enable_plugins!`, or via config `plugins.enabled`. Use `dependencies: [{ name: :other, version: '>=1.0.0' }]`, `requires: '1.2.3'`, `config_schema` for validated options, and `route_prefix` to isolate plugin routes. See `docs/FIRST_PLUGIN_GUIDE.md` for a full walkthrough.
261
+
262
+ ### **Parameter Validation**
263
+
264
+ Validate required parameters automatically:
265
+
266
+ ```ruby
267
+ class MyAPI < Takagi::Base
268
+ post '/sensor/reading' do
269
+ # Raises ArgumentError if any parameter is missing
270
+ validate_params :temperature, :humidity, :timestamp
271
+
272
+ created(
273
+ reading_id: save_reading(params),
274
+ temperature: params[:temperature]
275
+ )
276
+ end
277
+ end
278
+ ```
279
+
280
+ ### **Auto-extracted Parameters**
281
+
282
+ Parameters are automatically available without explicit block arguments:
283
+
284
+ ```ruby
285
+ class MyAPI < Takagi::Base
286
+ # Before: Had to specify params in arguments
287
+ get '/old/:id' do |request, params|
288
+ { id: params[:id] }
289
+ end
290
+
291
+ # After: params available automatically!
292
+ get '/new/:id' do
293
+ json id: params[:id], name: "Resource #{params[:id]}"
294
+ end
295
+
296
+ # Still works with explicit args if needed
297
+ get '/mixed/:id' do |request, params|
298
+ json id: params[:id], method: request.method
299
+ end
300
+ end
301
+ ```
302
+
303
+ ### **Request Inspection Helpers**
304
+
305
+ Easily inspect request properties:
306
+
307
+ ```ruby
308
+ class MyAPI < Takagi::Base
309
+ get '/data' do
310
+ # Check request method
311
+ return json(method: 'GET') if request.get?
312
+
313
+ # Check Accept header
314
+ if request.accept?('application/json')
315
+ json message: 'JSON format'
316
+ elsif request.accept?('application/cbor')
317
+ json message: 'CBOR format'
318
+ end
319
+ end
320
+
321
+ get '/search' do
322
+ # Access query parameters
323
+ query = request.query_params
324
+ json query: query, results: search(query['q'])
325
+ end
326
+
327
+ post '/upload' do
328
+ # Get content format
329
+ format = request.content_format
330
+ json received_format: format
331
+ end
332
+ end
333
+ ```
334
+
335
+ **Available request helpers:**
336
+ - `request.get?` / `post?` / `put?` / `delete?` / `observe?` - Check request method
337
+ - `request.accept?(format)` - Check if request accepts a format
338
+ - `request.content_format` - Get Content-Format option
339
+ - `request.query_params` - Get query parameters as hash
340
+ - `request.option(number)` - Get CoAP option by number
341
+ - `request.option?(number)` - Check if option exists
342
+
343
+ ### **Early Returns with `halt`**
344
+
345
+ Use `halt` for cleaner early returns:
346
+
347
+ ```ruby
348
+ class MyAPI < Takagi::Base
349
+ get '/resource/:id' do
350
+ halt not_found('Not found') unless exists?(params[:id])
351
+ halt forbidden('Access denied') unless can_access?(params[:id])
352
+
353
+ # Main logic only executes if checks pass
354
+ json data: fetch_resource(params[:id])
355
+ end
356
+ end
357
+ ```
358
+
359
+ ### **Observable Resources**
360
+
361
+ Make resources observable by clients using the `observable` method:
362
+
363
+ ```ruby
364
+ class MyAPI < Takagi::Base
365
+ reactor do
366
+ # Server-side: Offer an observable resource
367
+ observable '/sensor/temp' do
368
+ core { rt 'sensor.temperature'; obs true }
369
+ json temperature: read_sensor, unit: 'celsius'
370
+ end
371
+
372
+ # Client-side: Subscribe to a remote observable
373
+ observe 'coap://remote-server/sensor' do |request, params|
374
+ Takagi.logger.info "Received: #{params.inspect}"
375
+ end
376
+ end
377
+ end
378
+ ```
379
+
380
+ **Note:**
381
+ - `observable` = Server-side (offer observations to clients)
382
+ - `observe` = Client-side (subscribe to remote resources)
383
+
55
384
  ---
56
385
 
57
- ## Sending Requests
386
+ ## Sending Requests (Client API)
387
+
388
+ Takagi provides a unified, Ruby-idiomatic client API for sending CoAP requests over both UDP and TCP.
389
+
390
+ ### **Unified Client with Auto-Cleanup**
391
+
392
+ The client follows Ruby conventions (like `File.open`) with block-based initialization for automatic resource cleanup:
393
+
394
+ ```ruby
395
+ # Block-based initialization (recommended - auto-cleanup)
396
+ Takagi::Client.new('coap://localhost:5683') do |client|
397
+ client.get('/temperature')
398
+ end # Automatically cleaned up
399
+
400
+ # Or with TCP
401
+ Takagi::Client.new('coap+tcp://localhost:5683') do |client|
402
+ client.get('/temperature')
403
+ end
404
+
405
+ # Explicit protocol selection
406
+ Takagi::Client.new('localhost:5683', protocol: :tcp) do |client|
407
+ client.get('/temperature')
408
+ end
409
+ ```
410
+
411
+ ### **Response Wrapper with Convenience Methods**
412
+
413
+ Responses are wrapped with intuitive status checking methods:
414
+
415
+ ```ruby
416
+ Takagi::Client.new('coap://localhost:5683') do |client|
417
+ client.get('/temperature') do |response|
418
+ if response.success?
419
+ puts "Temperature: #{response.payload}"
420
+ puts "Status: #{response.code_name}" # "2.05 Content"
421
+ elsif response.not_found?
422
+ puts "Resource not found"
423
+ elsif response.error?
424
+ puts "Error: #{response.code_name}"
425
+ end
426
+ end
427
+ end
428
+ ```
429
+
430
+ **Available response methods:**
431
+ - **Status checking:** `success?`, `error?`, `client_error?`, `server_error?`
432
+ - **Specific codes:** `ok?`, `created?`, `deleted?`, `changed?`, `valid?`, `not_found?`, `bad_request?`, `unauthorized?`, `forbidden?`, `internal_server_error?`
433
+ - **Data access:** `payload`, `code`, `code_name`, `options`, `token`
434
+ - **JSON support:** `json`, `json?`, `content_format`
435
+
436
+ ### **JSON Convenience Methods**
437
+
438
+ Simplified JSON handling with automatic encoding and decoding:
439
+
440
+ ```ruby
441
+ Takagi::Client.new('coap://localhost:5683') do |client|
442
+ # Automatic JSON encoding
443
+ client.post_json('/data', {temp: 25, humidity: 60})
444
+
445
+ # Automatic JSON parsing
446
+ client.get_json('/data') do |data|
447
+ puts data['temp'] # Already parsed!
448
+ end
449
+
450
+ # Or check content type manually
451
+ client.get('/data') do |response|
452
+ if response.json?
453
+ data = response.json
454
+ puts data['temp']
455
+ end
456
+ end
457
+ end
458
+ ```
459
+
460
+ ### **All HTTP-Like Methods**
461
+
462
+ The client supports all standard CoAP methods:
463
+
464
+ ```ruby
465
+ Takagi::Client.new('coap://localhost:5683') do |client|
466
+ # GET request
467
+ client.get('/sensor/1') do |response|
468
+ puts response.payload if response.success?
469
+ end
470
+
471
+ # POST request
472
+ client.post('/sensor', 'temperature=25') do |response|
473
+ puts "Created: #{response.created?}"
474
+ end
475
+
476
+ # PUT request
477
+ client.put('/sensor/1', 'temperature=26') do |response|
478
+ puts "Changed: #{response.changed?}"
479
+ end
480
+
481
+ # DELETE request
482
+ client.delete('/sensor/1') do |response|
483
+ puts "Deleted: #{response.deleted?}"
484
+ end
485
+ end
486
+ ```
487
+
488
+ ### **Readable Default Output**
489
+
490
+ Without a block, responses print human-readable output:
491
+
492
+ ```ruby
493
+ Takagi::Client.new('coap://localhost:5683') do |client|
494
+ client.get('/resource')
495
+ # Prints: [2.05 Content] Hello World
496
+
497
+ client.get('/missing')
498
+ # Prints: [ERROR 4.04 Not Found] Resource not available
499
+ end
500
+ ```
501
+
502
+ ### **Using `coap-client` CLI**
503
+
504
+ For quick testing, you can also use the standard CoAP command-line tools:
58
505
 
59
- ### **Using `coap-client`**
60
506
  ```sh
61
507
  coap-client -m get coap://localhost:5683/sensor/1
62
508
  ```
@@ -66,23 +512,144 @@ coap-client -m post coap://localhost:5683/sensor -e '{"value":42}'
66
512
 
67
513
  ---
68
514
 
69
- ## Features & Modules
515
+ ## CoAP Protocol Registry System
516
+
517
+ Takagi includes an extensible registry system for protocol constants, providing a plugin-friendly architecture where custom methods, response codes, options, and content formats can be registered.
518
+
519
+ ## Plugin System
520
+
521
+ Takagi ships with an EventBus-backed plugin manager for runtime extensions and future clustering.
522
+
523
+ - **Auto-discovery**: Finds plugins under `Takagi::Plugins::*` and `takagi-plugin-*` gems (configurable via `plugins.auto_discover`).
524
+ - **Config + DSL**: Declare in code (`plugin MyPlugin, opt: 1`) or YAML:
525
+ ```yaml
526
+ plugins:
527
+ auto_discover: true
528
+ enabled:
529
+ - name: request_logging
530
+ options:
531
+ sample_rate: 0.1
532
+ - prometheus
533
+ ```
534
+ - **Lifecycle hooks**: Plugins may implement `before_apply/apply/after_apply` (plus `before_unload/shutdown`). Events publish to `hooks.plugin_*` on the EventBus.
535
+ - **Deps & versions**: `metadata` supports `dependencies:` and `requires:`; dependencies auto-enable first and versions are checked.
536
+ - **Config schema**: Optional `config_schema` validates types/enums/ranges and fills defaults before apply.
537
+ - **Hooked internals**: Core emits `hooks.*` for registry changes, route additions, middleware before/after, server start/stop, Observe subscribe/notify—ready for observability across nodes.
538
+
539
+ Quick start:
540
+ ```ruby
541
+ class MyApp < Takagi::Base
542
+ plugin Takagi::Plugins::RequestLogging, sample_rate: 0.2
543
+ end
70
544
 
71
- | Feature | Description | Status |
72
- |-----------------|--------------------------------|--------|
73
- | **CoAP API** | Define REST-like CoAP routes | ✅ Ready |
74
- | **Sequel DB** | Store IoT data in PostgreSQL, SQLite, etc. | ✅ Ready |
75
- | **Notifications** | Redis, HTTP, ZeroMQ messaging | 🔄 WIP |
76
- | **Buffering** | Store messages before processing | 🔄 Planned |
77
- | **Compression** | Reduce payload size | 🔄 Planned |
545
+ MyApp.run! # enables plugins from DSL + config
546
+ ```
547
+
548
+ ### **Using Protocol Constants**
549
+
550
+ All CoAP constants are organized into registries that follow RFC specifications:
551
+
552
+ ```ruby
553
+ # Method codes (RFC 7252 §12.1.1)
554
+ Takagi::CoAP::Registries::Method::GET # => 1
555
+ Takagi::CoAP::Registries::Method::POST # => 2
556
+
557
+ # Response codes (RFC 7252 §12.1.2)
558
+ Takagi::CoAP::Registries::Response::CONTENT # => 69 (2.05)
559
+ Takagi::CoAP::Registries::Response::NOT_FOUND # => 132 (4.04)
560
+
561
+ # Options (RFC 7252 §5.10)
562
+ Takagi::CoAP::Registries::Option::URI_PATH # => 11
563
+ Takagi::CoAP::Registries::Option::CONTENT_FORMAT # => 12
564
+
565
+ # Content formats (RFC 7252 §12.3)
566
+ Takagi::CoAP::Registries::ContentFormat::JSON # => 50
567
+ Takagi::CoAP::Registries::ContentFormat::TEXT_PLAIN # => 0
568
+ ```
569
+
570
+ ### **Code Conversion Helpers**
571
+
572
+ Convert between different representations easily:
573
+
574
+ ```ruby
575
+ # Convert code to human-readable string
576
+ Takagi::CoAP::CodeHelpers.to_string(69) # => "2.05 Content"
577
+ Takagi::CoAP::CodeHelpers.to_string(:get) # => "GET"
578
+
579
+ # Convert to numeric code
580
+ Takagi::CoAP::CodeHelpers.to_numeric(:get) # => 1
581
+ Takagi::CoAP::CodeHelpers.to_numeric("2.05") # => 69
582
+
583
+ # Status checking
584
+ Takagi::CoAP::Registries::Response.success?(69) # => true (2.xx)
585
+ Takagi::CoAP::Registries::Response.client_error?(132) # => true (4.xx)
586
+ ```
587
+
588
+ ### **Plugin Development**
589
+
590
+ Extend the protocol by registering new constants at runtime:
591
+
592
+ ```ruby
593
+ # Register a custom method (RFC 8132 - FETCH)
594
+ Takagi::CoAP::Registries::Method.register(5, 'FETCH', :fetch, rfc: 'RFC 8132 §2')
595
+
596
+ # Now available as constant
597
+ Takagi::CoAP::Registries::Method::FETCH # => 5
598
+
599
+ # Register custom response codes
600
+ Takagi::CoAP::Registries::Response.register(231, '7.07 Custom Code', :custom_code)
601
+
602
+ # Register custom options (e.g., for Block-Wise Transfer RFC 7959)
603
+ Takagi::CoAP::Registries::Option.register(23, 'Block2', :block2, rfc: 'RFC 7959 §2.2')
604
+ ```
605
+
606
+ **Benefits:**
607
+ - Single source of truth for all CoAP constants
608
+ - Plugin-friendly architecture for extending with new RFCs
609
+ - Self-documenting code (no magic numbers)
610
+ - RFC-aligned structure with inline documentation
611
+ - Type-safe with validation methods
78
612
 
79
613
  ---
80
614
 
81
- ## Roadmap
615
+ ## Features & Modules
82
616
 
83
- **Core framework (CoAP, Sequel, notifications)**
84
- 🔜 **Web UI for data visualization**
85
- 🔜 **More integrations: NATS, MQTT...**
617
+ ### Server Features
618
+
619
+ | Feature | Description | Status |
620
+ |----------------------------------|-------------|--------|
621
+ | **CoAP API (RFC 7252)** | Define REST-like CoAP routes with Sinatra-like DSL | ✅ Ready |
622
+ | **CoRE metadata DSL (RFC 6690)** | Describe discovery attributes inline or at boot | ✅ Ready |
623
+ | **Observe (RFC 7641)** | Offer server push and subscribe to remote feeds | ✅ Ready |
624
+ | **CoAP over TCP (RFC 8323)** | Reliable transport for constrained clients | ✅ Ready |
625
+ | **Response Helpers** | Semantic helpers like `json`, `created`, `deleted` | ✅ Ready |
626
+ | **Error Helpers** | Easy error responses with `not_found`, `forbidden`, etc. | ✅ Ready |
627
+ | **Auto-extracted Params** | No need to specify params in block arguments | ✅ Ready |
628
+ | **Request Helpers** | Inspect requests with `accept?`, `query_params`, etc. | ✅ Ready |
629
+ | **Validation Helpers** | Validate params with `validate_params` | ✅ Ready |
630
+ | **Early Returns** | Use `halt` for cleaner control flow | ✅ Ready |
631
+ | **Observable/Observe** | Clear distinction: `observable` (server), `observe` (client) | ✅ Ready |
632
+
633
+ ### Client Features
634
+
635
+ | Feature | Description | Status |
636
+ |----------------------------------|-------------|--------|
637
+ | **Unified Client API** | Single interface for UDP and TCP with auto-protocol detection | ✅ Ready |
638
+ | **Block-based Initialization** | Auto-cleanup with Ruby-idiomatic block pattern | ✅ Ready |
639
+ | **Response Wrapper** | Convenient status checking with `success?`, `error?`, etc. | ✅ Ready |
640
+ | **JSON Convenience Methods** | Automatic JSON encoding/decoding with `post_json`, `get_json` | ✅ Ready |
641
+ | **Readable Default Output** | Human-readable response formatting by default | ✅ Ready |
642
+ | **All CoAP Methods** | Support for GET, POST, PUT, DELETE over UDP/TCP | ✅ Ready |
643
+
644
+ ### Protocol & Extension Features
645
+
646
+ | Feature | Description | Status |
647
+ |----------------------------------|-------------|--------|
648
+ | **CoAP Registry System** | Extensible registry for protocol constants | ✅ Ready |
649
+ | **Protocol Constants** | RFC-aligned constants for methods, responses, options | ✅ Ready |
650
+ | **Code Helpers** | Convert between numeric codes and human-readable strings | ✅ Ready |
651
+ | **Plugin Architecture** | Register custom methods, options, and response codes | ✅ Ready |
652
+ | **RFC Documentation** | Inline RFC references for all constants | ✅ Ready |
86
653
 
87
654
  ---
88
655
 
@@ -98,7 +665,7 @@ bundle install
98
665
 
99
666
  Run tests:
100
667
  ```sh
101
- rspec
668
+ bundle exec rspec
102
669
  ```
103
670
 
104
671
  ---