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
data/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Takagi – Lightweight CoAP Framework for Ruby
|
|
2
2
|
|
|
3
|
-
<!---
|
|
4
3
|
[](https://rubygems.org/gems/takagi)
|
|
5
|
-
-->
|
|
6
4
|
[](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
|
-
🔹 **
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
##
|
|
615
|
+
## Features & Modules
|
|
82
616
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
---
|