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/ROADMAP.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Takagi Roadmap
|
|
2
|
+
|
|
3
|
+
## Takagi 1.0 – Core CoAP Framework (Current Phase)
|
|
4
|
+
**Goal:** A functional CoAP server & client, usable for IoT and edge computing.
|
|
5
|
+
|
|
6
|
+
**Internal definition:** Sinatra for CoAP
|
|
7
|
+
|
|
8
|
+
- [x] **Takagi Server** → Routing, dynamic paths (`/devices/:id`), basic message parsing.
|
|
9
|
+
- [x] **Takagi Client** → Enables communication with the Takagi Server.
|
|
10
|
+
- [x] **Basic support for URL ID parameters & payloads.**
|
|
11
|
+
- [x] **Logging and debugging for easier development.**
|
|
12
|
+
- [ ] **Sequel support for seamless storing data from CoAP**
|
|
13
|
+
- [ ] **Better error handling & stability improvements.**
|
|
14
|
+
- [ ] **Extended routing DSL (e.g., wildcards).**
|
|
15
|
+
- [ ] **Tests and RFC compatibility checks.**
|
|
16
|
+
- [ ] **Push Notifications (Observe)**
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Takagi-Device (Spiegel) – IoT Device Management Module (Post-1.0)
|
|
21
|
+
**Goal:** A full-fledged device management system over CoAP.
|
|
22
|
+
|
|
23
|
+
**Internal definition:** Know everything about your devices like Spike Spiegel about his enemies
|
|
24
|
+
|
|
25
|
+
- [ ] **Device Registration** → like `/register` endpoint.
|
|
26
|
+
- [ ] **Real-time Monitoring** → like `/status` endpoint.
|
|
27
|
+
- [ ] **Device Authentication** → Basic token-based security.
|
|
28
|
+
- [ ] **Notify clients about state changes trough Observer**
|
|
29
|
+
- [ ] **Define a standardized API for devices.**
|
|
30
|
+
- [ ] **Integrate with more databases (InfluxDB?).**
|
|
31
|
+
- [ ] **Scalability & performance testing.**
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Takagi-Sinatra – Web Dashboard
|
|
36
|
+
**Goal:** Web integration visualization & admin panel for IoT device management.
|
|
37
|
+
|
|
38
|
+
**Internal definition:** Because Takagi and Sinatra would be great duo!
|
|
39
|
+
|
|
40
|
+
- [ ] **Show online/offline devices.**
|
|
41
|
+
- [ ] **Visualize metrics (battery, signal strength, temperature, etc.).**
|
|
42
|
+
- [ ] **CRUD operations for managing devices.**
|
|
43
|
+
- [ ] **Charts & real-time data streaming.**
|
|
44
|
+
- [ ] **Authentication & user access control.**
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Takagi-Zephyr – Embedded Integration with ZephyrOS
|
|
49
|
+
**Goal:** Allow communicate with Takagi on low-power IoT devices with Spiegel management.
|
|
50
|
+
|
|
51
|
+
**Internal definition:** Because software is not everything.
|
|
52
|
+
|
|
53
|
+
- [ ] **Develop Spiegel-able library for ZephyrOS**
|
|
54
|
+
- [ ] **Minimalist footprint for embedded systems.**
|
|
55
|
+
- [ ] **Test edge computing use-cases.**
|
data/Rakefile
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
require
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
9
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
12
|
-
task default: %i[spec
|
|
12
|
+
task default: %i[spec]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# D = Steep::Diagnostic
|
|
4
|
+
#
|
|
5
|
+
# target :lib do
|
|
6
|
+
# signature "sig"
|
|
7
|
+
# ignore_signature "sig/test"
|
|
8
|
+
#
|
|
9
|
+
# check "lib" # Directory name
|
|
10
|
+
# check "path/to/source.rb" # File name
|
|
11
|
+
# check "app/models/**/*.rb" # Glob
|
|
12
|
+
# # ignore "lib/templates/*.rb"
|
|
13
|
+
#
|
|
14
|
+
# # library "pathname" # Standard libraries
|
|
15
|
+
# # library "strong_json" # Gems
|
|
16
|
+
#
|
|
17
|
+
# # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
|
|
18
|
+
# # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
|
|
19
|
+
# # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
|
|
20
|
+
# # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
|
|
21
|
+
# # configure_code_diagnostics do |hash| # You can setup everything yourself
|
|
22
|
+
# # hash[D::Ruby::NoMethod] = :information
|
|
23
|
+
# # end
|
|
24
|
+
# end
|
|
25
|
+
|
|
26
|
+
# target :test do
|
|
27
|
+
# unreferenced! # Skip type checking the `lib` code when types in `test` target is changed
|
|
28
|
+
# signature "sig/test" # Put RBS files for tests under `sig/test`
|
|
29
|
+
# check "test" # Type check Ruby scripts under `test`
|
|
30
|
+
#
|
|
31
|
+
# configure_code_diagnostics(D::Ruby.lenient) # Weak type checking for test code
|
|
32
|
+
#
|
|
33
|
+
# # library "pathname" # Standard libraries
|
|
34
|
+
# end
|
|
35
|
+
|
|
36
|
+
target :takagi do
|
|
37
|
+
check 'lib'
|
|
38
|
+
signature 'sig'
|
|
39
|
+
end
|
data/bin/takagi-dev
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Takagi CLI - Minimal commands for server management and information
|
|
5
|
+
# Project generation and advanced features available in takagi-devtools gem
|
|
6
|
+
|
|
7
|
+
require_relative '../lib/takagi'
|
|
8
|
+
require_relative '../lib/takagi/branding'
|
|
9
|
+
|
|
10
|
+
command = ARGV[0]
|
|
11
|
+
args = ARGV[1..]
|
|
12
|
+
|
|
13
|
+
case command
|
|
14
|
+
when 'server', 's'
|
|
15
|
+
# Parse options
|
|
16
|
+
port = 5683
|
|
17
|
+
protocols = [:udp]
|
|
18
|
+
banner = true
|
|
19
|
+
app_file = 'app.rb'
|
|
20
|
+
|
|
21
|
+
i = 0
|
|
22
|
+
while i < args.length
|
|
23
|
+
case args[i]
|
|
24
|
+
when '-p', '--port'
|
|
25
|
+
port = args[i + 1].to_i
|
|
26
|
+
i += 1
|
|
27
|
+
when '--tcp'
|
|
28
|
+
protocols << :tcp unless protocols.include?(:tcp)
|
|
29
|
+
when '--udp'
|
|
30
|
+
protocols << :udp unless protocols.include?(:udp)
|
|
31
|
+
when '--no-banner'
|
|
32
|
+
banner = false
|
|
33
|
+
when '-f', '--file'
|
|
34
|
+
app_file = args[i + 1]
|
|
35
|
+
i += 1
|
|
36
|
+
end
|
|
37
|
+
i += 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Load the application
|
|
41
|
+
unless File.exist?(app_file)
|
|
42
|
+
puts Takagi::Branding.error("Application file not found: #{app_file}")
|
|
43
|
+
puts Takagi::Branding.info("Create #{app_file} or specify with -f FILE")
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts Takagi::Branding.log("Loading application from #{app_file}...")
|
|
48
|
+
require File.expand_path(app_file)
|
|
49
|
+
|
|
50
|
+
# Find the application class (first class inheriting from Takagi::Base)
|
|
51
|
+
app_class = ObjectSpace.each_object(Class).find { |klass| klass < Takagi::Base && klass != Takagi::Base }
|
|
52
|
+
|
|
53
|
+
unless app_class
|
|
54
|
+
puts Takagi::Branding.error("No Takagi::Base subclass found in #{app_file}")
|
|
55
|
+
puts Takagi::Branding.info("Define a class that inherits from Takagi::Base")
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Start the server
|
|
60
|
+
app_class.run!(port: port, protocols: protocols, banner: banner)
|
|
61
|
+
|
|
62
|
+
when 'routes', 'r'
|
|
63
|
+
# Load the application
|
|
64
|
+
app_file = 'app.rb'
|
|
65
|
+
args.each_with_index do |arg, i|
|
|
66
|
+
app_file = args[i + 1] if arg == '-f' || arg == '--file'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless File.exist?(app_file)
|
|
70
|
+
puts Takagi::Branding.error("Application file not found: #{app_file}")
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
require File.expand_path(app_file)
|
|
75
|
+
|
|
76
|
+
# Find the application class
|
|
77
|
+
app_class = ObjectSpace.each_object(Class).find { |klass| klass < Takagi::Base && klass != Takagi::Base }
|
|
78
|
+
|
|
79
|
+
unless app_class
|
|
80
|
+
puts Takagi::Branding.error("No Takagi::Base subclass found")
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
puts Takagi::Branding.section('Registered Routes')
|
|
85
|
+
puts
|
|
86
|
+
|
|
87
|
+
router = app_class.router
|
|
88
|
+
routes = router.instance_variable_get(:@routes)
|
|
89
|
+
|
|
90
|
+
if routes.empty?
|
|
91
|
+
puts Takagi::Branding.info('No routes registered')
|
|
92
|
+
else
|
|
93
|
+
# Group routes by method
|
|
94
|
+
grouped = routes.values.group_by(&:method).sort_by { |method, _| method }
|
|
95
|
+
|
|
96
|
+
grouped.each do |method, entries|
|
|
97
|
+
entries.each do |entry|
|
|
98
|
+
metadata_str = if entry.metadata.any?
|
|
99
|
+
tags = entry.metadata.map { |k, v| "#{k}:#{v}" }.join(', ')
|
|
100
|
+
" [#{tags}]"
|
|
101
|
+
else
|
|
102
|
+
''
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
puts Takagi::Branding.log("#{method.ljust(8)} #{entry.path}#{metadata_str}")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
puts
|
|
110
|
+
puts Takagi::Branding.success("#{routes.size} routes registered")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
when 'version', 'v', '-v', '--version'
|
|
114
|
+
version = defined?(Takagi::VERSION) ? Takagi::VERSION : '1.0.0'
|
|
115
|
+
puts Takagi::Branding::LOGO_WITH_NAME + " v#{version}"
|
|
116
|
+
|
|
117
|
+
when 'help', 'h', '-h', '--help', nil
|
|
118
|
+
puts <<~HELP
|
|
119
|
+
|
|
120
|
+
#{Takagi::Branding::BANNER}
|
|
121
|
+
|
|
122
|
+
Usage: takagi COMMAND [options]
|
|
123
|
+
|
|
124
|
+
Commands:
|
|
125
|
+
server, s Start the Takagi server
|
|
126
|
+
routes, r List all registered routes
|
|
127
|
+
version, v Show Takagi version
|
|
128
|
+
help, h Show this help message
|
|
129
|
+
|
|
130
|
+
Server Options:
|
|
131
|
+
-p, --port PORT Port to bind to (default: 5683)
|
|
132
|
+
--tcp Enable TCP protocol
|
|
133
|
+
--udp Enable UDP protocol (default)
|
|
134
|
+
--no-banner Disable startup banner
|
|
135
|
+
-f, --file FILE Application file (default: app.rb)
|
|
136
|
+
|
|
137
|
+
Routes Options:
|
|
138
|
+
-f, --file FILE Application file (default: app.rb)
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
takagi server # Start server on port 5683 (UDP)
|
|
142
|
+
takagi server -p 5684 # Start on custom port
|
|
143
|
+
takagi server --tcp --udp # Enable both protocols
|
|
144
|
+
takagi server --no-banner # No banner
|
|
145
|
+
takagi routes # List all routes
|
|
146
|
+
takagi version # Show version
|
|
147
|
+
|
|
148
|
+
For project generation and advanced features, install:
|
|
149
|
+
gem install takagi-devtools
|
|
150
|
+
|
|
151
|
+
#{Takagi::Branding::WAVE_LINE}
|
|
152
|
+
|
|
153
|
+
HELP
|
|
154
|
+
|
|
155
|
+
else
|
|
156
|
+
puts Takagi::Branding.error("Unknown command: #{command}")
|
|
157
|
+
puts Takagi::Branding.info("Run 'takagi help' for usage information")
|
|
158
|
+
exit 1
|
|
159
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# First Plugin Guide
|
|
2
|
+
|
|
3
|
+
Build a Takagi plugin that adds routes, content-formats, and even transports, without touching core code.
|
|
4
|
+
|
|
5
|
+
## Plugin contract (what you implement)
|
|
6
|
+
- Module (ideally `Takagi::Plugins::<Name>`) with `.apply(app, opts = {})`.
|
|
7
|
+
- Optional lifecycle: `.before_apply(app, opts)`, `.after_apply(app, opts)`, `.before_unload(app)`, `.shutdown(app)`.
|
|
8
|
+
- Optional metadata via `.metadata` hash: `{ name:, description:, requires:, dependencies: [] }`.
|
|
9
|
+
- Optional config schema via `.config_schema` hash: keys → rules (`:type`, `:required`, `:default`, `:enum`, `:range`, `:validate` proc).
|
|
10
|
+
- Optional route prefix isolation via `metadata[:route_prefix]` to auto-prefix routes registered by the plugin.
|
|
11
|
+
- Enable via `plugin :name, opts` in your app + `enable_plugins!`, or via config (`Takagi.config.plugins.enabled`). Auto-discovery will pick up `Takagi::Plugins::*` and gems named `takagi-plugin-*`.
|
|
12
|
+
- You can order plugins with `plugin :name, order: 10` (lower runs first).
|
|
13
|
+
|
|
14
|
+
## Minimal skeleton
|
|
15
|
+
```ruby
|
|
16
|
+
# lib/takagi/plugins/hello.rb
|
|
17
|
+
module Takagi
|
|
18
|
+
module Plugins
|
|
19
|
+
module Hello
|
|
20
|
+
def self.metadata
|
|
21
|
+
{ name: :hello, description: 'Adds /hello endpoint' }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.config_schema
|
|
25
|
+
{ greeting: { type: :string, default: 'Hello' } }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Optional: isolate routes under /hello
|
|
29
|
+
def self.metadata
|
|
30
|
+
{ name: :hello, route_prefix: '/hello' }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.apply(app, opts = {})
|
|
34
|
+
greeting = opts[:greeting] || 'Hello'
|
|
35
|
+
app.get '/hello' do
|
|
36
|
+
respond message: "#{greeting}, CoAP!"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Enable it:
|
|
45
|
+
```ruby
|
|
46
|
+
class MyAPI < Takagi::Base
|
|
47
|
+
plugin :hello, greeting: 'Ahoy'
|
|
48
|
+
end
|
|
49
|
+
MyAPI.enable_plugins!
|
|
50
|
+
```
|
|
51
|
+
Or via config: add `{ name: :hello, options: { greeting: 'Hi' } }` to `Takagi.config.plugins.enabled`.
|
|
52
|
+
|
|
53
|
+
## Add a new content-format + serializer
|
|
54
|
+
```ruby
|
|
55
|
+
module Takagi
|
|
56
|
+
module Plugins
|
|
57
|
+
module SenmlCbor
|
|
58
|
+
def self.metadata
|
|
59
|
+
{ name: :senml_cbor, description: 'SenML CBOR support', version: '1.0.0' }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.apply(_app, _opts = {})
|
|
63
|
+
serializer = Class.new(Takagi::Serialization::Base) do
|
|
64
|
+
def encode(data) CBOR.encode(data) end
|
|
65
|
+
def decode(bytes) CBOR.decode(bytes) end
|
|
66
|
+
def content_type; 'application/senml+cbor'; end
|
|
67
|
+
def content_format_code; 112; end # example code
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Takagi::Serialization::Registry.register(serializer.new.content_format_code, serializer)
|
|
71
|
+
Takagi::CoAP::Registries::ContentFormat.register(serializer.new.content_format_code, serializer.new.content_type, :senml_cbor)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
Use it in routes by setting `ct:` metadata or `respond(payload, force: 112)`; `respond` negotiates with `Accept` and returns `4.06/4.15` when unsupported.
|
|
78
|
+
|
|
79
|
+
## Add a new protocol/transport
|
|
80
|
+
```ruby
|
|
81
|
+
module Takagi
|
|
82
|
+
module Plugins
|
|
83
|
+
module Quic
|
|
84
|
+
def self.metadata
|
|
85
|
+
{ name: :quic, description: 'CoAP over QUIC' }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.apply(app, opts = {})
|
|
89
|
+
Takagi::Server::Registry.register(:quic, Takagi::Server::Quic, rfc: 'RFC xxxx')
|
|
90
|
+
Takagi::Network::Registry.register(:quic, Takagi::Network::Quic)
|
|
91
|
+
# Optionally auto-enable:
|
|
92
|
+
app.run!(protocols: (opts[:protocols] || [:udp, :tcp, :quic]))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
Implement `Takagi::Server::Quic` and `Takagi::Network::Quic` similar to UDP/TCP classes.
|
|
99
|
+
|
|
100
|
+
## Plugin-specific controllers/routes
|
|
101
|
+
Add routes directly in `.apply`:
|
|
102
|
+
```ruby
|
|
103
|
+
def self.apply(app, _opts = {})
|
|
104
|
+
app.get '/plugin/info', metadata: { ct: Takagi::CoAP::Registries::ContentFormat::JSON } do
|
|
105
|
+
respond name: 'my_plugin', version: '1.0.0'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
If you want a dedicated controller class, subclass `Takagi::Base` or `Takagi::Controller` and register its routes inside `.apply`.
|
|
110
|
+
|
|
111
|
+
## Register new CoAP codes/options (if needed)
|
|
112
|
+
- Methods: `Takagi::CoAP::Registries::Method.register(7, 'CUSTOM', :custom)`
|
|
113
|
+
- Responses: `Takagi::CoAP::Registries::Response.register(231, '7.07 Custom', :custom)` (helper methods are generated).
|
|
114
|
+
- Options: `Takagi::CoAP::Registries::Option.register(65000, 'Plugin-Option', :plugin_option)`
|
|
115
|
+
|
|
116
|
+
## Hooks and events
|
|
117
|
+
Hooks emitted during plugin lifecycle: `:plugin_registered`, `:plugin_enabling`, `:plugin_enabled`, `:plugin_disabling`, `:plugin_disabled`, `:plugin_error` (see `docs/HOOKS.md`). Subscribe if you need side effects/logging. You can also publish EventBus events under `plugin.*` if needed.
|
|
118
|
+
|
|
119
|
+
### Lifecycle hooks (and when to use them)
|
|
120
|
+
- `:plugin_registered` — after a plugin module is registered. Use to log or audit availability.
|
|
121
|
+
- `:plugin_enabling` — before `.apply` runs. Use to perform pre-flight checks or metrics.
|
|
122
|
+
- `:plugin_enabled` — after `.apply` completes. Use to announce availability (e.g., publish `plugin.started`).
|
|
123
|
+
- `:plugin_disabling` — before shutdown/unload. Use to stop background work or flush buffers.
|
|
124
|
+
- `:plugin_disabled` — after shutdown/unload. Use to log or emit “stopped” events.
|
|
125
|
+
- `:plugin_error` — when any lifecycle step raises. Use to alert/rollback.
|
|
126
|
+
|
|
127
|
+
Subscribe via `Takagi::Hooks.on(:event_name) { |payload| ... }` (see `docs/HOOKS.md`).
|
|
128
|
+
|
|
129
|
+
### Other useful hooks (see `docs/HOOKS.md` for payloads)
|
|
130
|
+
- Router/routes: `:router_route_added` — track dynamic route registration (metrics/debug).
|
|
131
|
+
- Middleware: `:middleware_before_call`, `:middleware_after_call` — wrap/measure middleware execution.
|
|
132
|
+
- Response build: `:before_response_build`, `:after_response_build` — observe how results become responses.
|
|
133
|
+
- Server lifecycle: `:server_starting`, `:server_stopped` — start/stop transport-adjacent resources.
|
|
134
|
+
- Worker lifecycle: `:controller_workers_started`, `:controller_workers_stopped` — manage thread-bound resources.
|
|
135
|
+
- Observe: `:observe_subscribed`, `:observe_unsubscribed`, `:observe_notify_start`, `:observe_notify_end` — instrument CoAP Observe flows.
|
|
136
|
+
- CoAP registries: `:coap_registry_registered`, `:coap_registry_cleared` — react to new methods/options/content-formats.
|
|
137
|
+
|
|
138
|
+
### Hook payloads and quick examples
|
|
139
|
+
Subscribe with `Takagi::Hooks.on(:hook) { |p| ... }`, emit with `Takagi::Hooks.emit(:hook, payload)`.
|
|
140
|
+
|
|
141
|
+
- `:coap_registry_registered` — keys: `registry`, `value`, `name`, `symbol`, `rfc`.
|
|
142
|
+
Example: `Takagi::Hooks.on(:coap_registry_registered) { |p| Takagi.logger.info("Registry #{p[:registry]} added #{p[:name]} (#{p[:value]})") }`
|
|
143
|
+
- `:coap_registry_cleared` — keys: `registry`.
|
|
144
|
+
Example: `Takagi::Hooks.on(:coap_registry_cleared) { |p| Takagi.logger.warn("Registry cleared: #{p[:registry]}") }`
|
|
145
|
+
- `:router_route_added` — keys: `method`, `path`, `entry`.
|
|
146
|
+
Example: `Takagi::Hooks.on(:router_route_added) { |p| Takagi.logger.info("Route #{p[:method]} #{p[:path]} added") }`
|
|
147
|
+
- `:middleware_before_call` — keys: `request`.
|
|
148
|
+
Example: `Takagi::Hooks.on(:middleware_before_call) { |p| metrics.increment(:mw_in) }`
|
|
149
|
+
- `:middleware_after_call` — keys: `request`, `response`.
|
|
150
|
+
Example: `Takagi::Hooks.on(:middleware_after_call) { |p| metrics.timing(:mw_latency, Time.now - p[:request].started_at) if p[:request].respond_to?(:started_at) }`
|
|
151
|
+
- `:before_response_build` — keys: `inbound`, `result`.
|
|
152
|
+
Example: `Takagi::Hooks.on(:before_response_build) { |p| Takagi.logger.debug("Building response from #{p[:result].class}") }`
|
|
153
|
+
- `:after_response_build` — keys: `inbound`, `response`, `result`.
|
|
154
|
+
Example: `Takagi::Hooks.on(:after_response_build) { |p| metrics.increment(:responses) }`
|
|
155
|
+
- `:server_starting` — keys: `protocol`, `port`.
|
|
156
|
+
Example: `Takagi::Hooks.on(:server_starting) { |p| Takagi.logger.info("Starting #{p[:protocol]} on #{p[:port]}") }`
|
|
157
|
+
- `:server_stopped` — keys: `protocol`, `port`.
|
|
158
|
+
Example: `Takagi::Hooks.on(:server_stopped) { |p| Takagi.logger.info("Stopped #{p[:protocol]} on #{p[:port]}") }`
|
|
159
|
+
- `:controller_workers_started` — keys: `controller`, `name`, `threads`.
|
|
160
|
+
Example: `Takagi::Hooks.on(:controller_workers_started) { |p| metrics.gauge(:worker_threads, p[:threads]) }`
|
|
161
|
+
- `:controller_workers_stopped` — keys: `controller`.
|
|
162
|
+
Example: `Takagi::Hooks.on(:controller_workers_stopped) { |p| Takagi.logger.info("Workers stopped for #{p[:controller]}") }`
|
|
163
|
+
- `:observe_subscribed` — keys: `path`, `subscription`.
|
|
164
|
+
Example: `Takagi::Hooks.on(:observe_subscribed) { |p| metrics.increment(:observe_subscriptions) }`
|
|
165
|
+
- `:observe_unsubscribed` — keys: `path`, `token`.
|
|
166
|
+
Example: `Takagi::Hooks.on(:observe_unsubscribed) { |p| metrics.increment(:observe_unsubscriptions) }`
|
|
167
|
+
- `:observe_notify_start` — keys: `path`, `subscribers`, `value`.
|
|
168
|
+
Example: `Takagi::Hooks.on(:observe_notify_start) { |p| metrics.gauge(:observers, p[:subscribers].size) }`
|
|
169
|
+
- `:observe_notify_end` — keys: `path`, `delivered`, `value`.
|
|
170
|
+
Example: `Takagi::Hooks.on(:observe_notify_end) { |p| metrics.increment(:observe_notifications, p[:delivered]) }`
|
|
171
|
+
- `:plugin_registered` — keys: `name`, `metadata`.
|
|
172
|
+
Example: `Takagi::Hooks.on(:plugin_registered) { |p| Takagi.logger.info("Plugin registered: #{p[:name]}") }`
|
|
173
|
+
- `:plugin_enabling` — keys: `name`, `metadata`, `options`.
|
|
174
|
+
Example: `Takagi::Hooks.on(:plugin_enabling) { |p| audit("enable #{p[:name]}", p[:options]) }`
|
|
175
|
+
- `:plugin_enabled` — keys: `name`, `metadata`.
|
|
176
|
+
Example: `Takagi::Hooks.on(:plugin_enabled) { |p| EventBus.publish('plugin.started', p) if defined?(EventBus) }`
|
|
177
|
+
- `:plugin_disabling` — keys: `name`, `metadata`.
|
|
178
|
+
Example: `Takagi::Hooks.on(:plugin_disabling) { |p| audit("disable #{p[:name]}") }`
|
|
179
|
+
- `:plugin_disabled` — keys: `name`, `metadata`.
|
|
180
|
+
Example: `Takagi::Hooks.on(:plugin_disabled) { |p| Takagi.logger.info("Plugin disabled: #{p[:name]}") }`
|
|
181
|
+
- `:plugin_error` — keys: `name`, `metadata`, `error`.
|
|
182
|
+
Example: `Takagi::Hooks.on(:plugin_error) { |p| Takagi.logger.error("Plugin error #{p[:name]}: #{p[:error]}") }`
|
|
183
|
+
|
|
184
|
+
### Hook call order (lifecycle sketch)
|
|
185
|
+
```
|
|
186
|
+
Plugin enable:
|
|
187
|
+
plugin_registered (when module is registered)
|
|
188
|
+
plugin_enabling
|
|
189
|
+
before_apply (optional, plugin hook)
|
|
190
|
+
apply (plugin logic runs)
|
|
191
|
+
after_apply (optional, plugin hook)
|
|
192
|
+
plugin_enabled
|
|
193
|
+
plugin_error (on any failure above)
|
|
194
|
+
|
|
195
|
+
Plugin disable:
|
|
196
|
+
plugin_disabling
|
|
197
|
+
before_unload (optional)
|
|
198
|
+
shutdown (optional)
|
|
199
|
+
plugin_disabled
|
|
200
|
+
plugin_error (on failure)
|
|
201
|
+
|
|
202
|
+
Server/workers:
|
|
203
|
+
server_starting -> controller_workers_started -> ...runtime... -> controller_workers_stopped -> server_stopped
|
|
204
|
+
|
|
205
|
+
Request/response:
|
|
206
|
+
middleware_before_call -> ...handler... -> before_response_build -> after_response_build -> middleware_after_call
|
|
207
|
+
|
|
208
|
+
Routes/registries:
|
|
209
|
+
router_route_added (when route registered)
|
|
210
|
+
coap_registry_registered (when registry extended)
|
|
211
|
+
coap_registry_cleared (mostly in tests/reset)
|
|
212
|
+
|
|
213
|
+
Observe:
|
|
214
|
+
observe_subscribed / observe_unsubscribed
|
|
215
|
+
observe_notify_start -> observe_notify_end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Checklist
|
|
219
|
+
1) Create `Takagi::Plugins::YourPlugin` with `.apply`.
|
|
220
|
+
2) (Optional) Add serializers/content-formats; register in both `Serialization::Registry` and `CoAP::Registries::ContentFormat`.
|
|
221
|
+
3) (Optional) Add transport: register server + network transport.
|
|
222
|
+
4) Add routes (or a controller) using `respond`, set `ct` metadata so discovery and negotiation stay aligned; use `route_prefix` if you want isolation.
|
|
223
|
+
5) Declare metadata/config schema; handle dependencies/`requires`/dependency versions.
|
|
224
|
+
6) Enable via `plugin :your_plugin, options, order: X` and call `enable_plugins!` (or rely on auto-discovery/config).
|
data/docs/HOOKS.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Hooks for Plugins (EventBus-backed)
|
|
2
|
+
|
|
3
|
+
Hooks are published on the EventBus under `hooks.<event>` and delivered to local consumers. Subscribe with `Takagi::Hooks.subscribe(event) { |payload| ... }` (internally uses `EventBus.consumer`), emit with `Takagi::Hooks.emit(event, payload)`. Payloads are sent with `freeze_body: false` to avoid freezing mutable objects; keep payloads small and serializable for future clustering.
|
|
4
|
+
|
|
5
|
+
Current events and payloads:
|
|
6
|
+
- `:coap_registry_registered` — { registry:, value:, name:, symbol:, rfc: }
|
|
7
|
+
- `:coap_registry_cleared` — { registry: }
|
|
8
|
+
- `:router_route_added` — { method:, path:, entry: }
|
|
9
|
+
- `:middleware_before_call` — { request: }
|
|
10
|
+
- `:middleware_after_call` — { request:, response: }
|
|
11
|
+
- `:before_response_build` — { inbound:, result: }
|
|
12
|
+
- `:after_response_build` — { inbound:, response:, result: }
|
|
13
|
+
- `:server_starting` — { protocol:, port: }
|
|
14
|
+
- `:server_stopped` — { protocol:, port: }
|
|
15
|
+
- `:controller_workers_started` — { controller:, name:, threads: }
|
|
16
|
+
- `:controller_workers_stopped` — { controller: }
|
|
17
|
+
- `:observe_subscribed` — { path:, subscription: }
|
|
18
|
+
- `:observe_unsubscribed` — { path:, token: }
|
|
19
|
+
- `:observe_notify_start` — { path:, subscribers:, value: }
|
|
20
|
+
- `:observe_notify_end` — { path:, delivered:, value: }
|
|
21
|
+
- `:plugin_registered` — { name:, metadata: }
|
|
22
|
+
- `:plugin_enabling` — { name:, metadata:, options: }
|
|
23
|
+
- `:plugin_enabled` — { name:, metadata: }
|
|
24
|
+
- `:plugin_disabling` — { name:, metadata: }
|
|
25
|
+
- `:plugin_disabled` — { name:, metadata: }
|
|
26
|
+
- `:plugin_error` — { name:, metadata:, error: }
|
|
27
|
+
|
|
28
|
+
Notes:
|
|
29
|
+
- Handlers are executed in-process; errors are logged and swallowed.
|
|
30
|
+
- EventBus forwarding publishes to `hooks.<event>` by default when EventBus is loaded.
|
|
31
|
+
- Hook payloads are simple hashes to keep them Ractor-friendly later.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example demonstrating the unified Takagi::Client API with multiple protocols
|
|
5
|
+
#
|
|
6
|
+
# This example shows the new unified API that supports:
|
|
7
|
+
# 1. Protocol auto-detection from URI scheme
|
|
8
|
+
# 2. Explicit protocol specification
|
|
9
|
+
# 3. Block-based auto-close pattern (recommended)
|
|
10
|
+
# 4. Manual lifecycle management
|
|
11
|
+
|
|
12
|
+
require_relative '../lib/takagi'
|
|
13
|
+
|
|
14
|
+
puts "Demonstrating Unified Takagi::Client API\n\n"
|
|
15
|
+
|
|
16
|
+
# Pattern 1: Protocol auto-detection from URI
|
|
17
|
+
puts 'Pattern 1: Protocol Auto-Detection from URI'
|
|
18
|
+
puts '=' * 50
|
|
19
|
+
|
|
20
|
+
# UDP client (coap:// scheme)
|
|
21
|
+
puts 'Creating UDP client from coap:// URI:'
|
|
22
|
+
client_udp = Takagi::Client.new('coap://localhost:5683')
|
|
23
|
+
puts ' Protocol: UDP (auto-detected)'
|
|
24
|
+
puts " Implementation: #{client_udp.instance_variable_get(:@impl).class}"
|
|
25
|
+
client_udp.close
|
|
26
|
+
|
|
27
|
+
# TCP client (coap+tcp:// scheme)
|
|
28
|
+
puts "\nCreating TCP client from coap+tcp:// URI:"
|
|
29
|
+
client_tcp = Takagi::Client.new('coap+tcp://localhost:5683')
|
|
30
|
+
puts ' Protocol: TCP (auto-detected)'
|
|
31
|
+
puts " Implementation: #{client_tcp.instance_variable_get(:@impl).class}"
|
|
32
|
+
client_tcp.close
|
|
33
|
+
puts
|
|
34
|
+
|
|
35
|
+
# Pattern 2: Explicit protocol specification
|
|
36
|
+
puts 'Pattern 2: Explicit Protocol Specification'
|
|
37
|
+
puts '=' * 50
|
|
38
|
+
|
|
39
|
+
# Explicitly specify TCP
|
|
40
|
+
puts 'Creating TCP client with protocol parameter:'
|
|
41
|
+
client = Takagi::Client.new('localhost:5683', protocol: :tcp)
|
|
42
|
+
puts ' Protocol: TCP (explicit)'
|
|
43
|
+
puts " Implementation: #{client.instance_variable_get(:@impl).class}"
|
|
44
|
+
client.close
|
|
45
|
+
|
|
46
|
+
# Explicitly specify UDP
|
|
47
|
+
puts "\nCreating UDP client with protocol parameter:"
|
|
48
|
+
client = Takagi::Client.new('localhost:5683', protocol: :udp)
|
|
49
|
+
puts ' Protocol: UDP (explicit)'
|
|
50
|
+
puts " Implementation: #{client.instance_variable_get(:@impl).class}"
|
|
51
|
+
client.close
|
|
52
|
+
puts
|
|
53
|
+
|
|
54
|
+
# Pattern 3: Block-based auto-close (RECOMMENDED)
|
|
55
|
+
puts 'Pattern 3: Block-Based Auto-Close (RECOMMENDED)'
|
|
56
|
+
puts '=' * 50
|
|
57
|
+
initial_threads = Thread.list.size
|
|
58
|
+
puts "Initial thread count: #{initial_threads}"
|
|
59
|
+
|
|
60
|
+
Takagi::Client.new('coap://localhost:5683') do |_client|
|
|
61
|
+
puts "Inside block. Thread count: #{Thread.list.size}"
|
|
62
|
+
puts 'Client protocol: UDP'
|
|
63
|
+
# Use the client...
|
|
64
|
+
# client.get('/resource')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sleep 0.2 # Give thread time to stop
|
|
68
|
+
puts "After block. Thread count: #{Thread.list.size}"
|
|
69
|
+
puts
|
|
70
|
+
|
|
71
|
+
# Pattern 4: Multiple protocols with thread leak prevention
|
|
72
|
+
puts 'Pattern 4: Multiple Protocols - Thread Leak Test'
|
|
73
|
+
puts '=' * 50
|
|
74
|
+
initial_threads = Thread.list.size
|
|
75
|
+
puts "Initial thread count: #{initial_threads}"
|
|
76
|
+
|
|
77
|
+
# Create and close multiple clients with different protocols
|
|
78
|
+
3.times do |i|
|
|
79
|
+
Takagi::Client.new('coap://localhost:5683') do |_client|
|
|
80
|
+
# UDP client work...
|
|
81
|
+
end
|
|
82
|
+
puts " UDP Client #{i + 1} closed"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
2.times do |i|
|
|
86
|
+
Takagi::Client.new('localhost:5683', protocol: :tcp) do |_client|
|
|
87
|
+
# TCP client work...
|
|
88
|
+
end
|
|
89
|
+
puts " TCP Client #{i + 1} closed"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sleep 0.5 # Give threads time to stop
|
|
93
|
+
final_threads = Thread.list.size
|
|
94
|
+
puts "Final thread count: #{final_threads}"
|
|
95
|
+
puts "Thread leak prevented: #{final_threads <= initial_threads + 1 ? 'YES ✓' : 'NO ✗'}"
|
|
96
|
+
puts
|
|
97
|
+
|
|
98
|
+
# Pattern 5: Error handling with auto-close
|
|
99
|
+
puts 'Pattern 5: Auto-Close Even With Errors'
|
|
100
|
+
puts '=' * 50
|
|
101
|
+
begin
|
|
102
|
+
Takagi::Client.new('coap://localhost:5683') do |clnt|
|
|
103
|
+
puts "Inside block. Client open: #{!clnt.closed?}"
|
|
104
|
+
raise 'Simulated error'
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
puts "Caught error: #{e.message}"
|
|
108
|
+
puts 'Client was still closed despite error'
|
|
109
|
+
end
|
|
110
|
+
puts
|
|
111
|
+
|
|
112
|
+
puts "\nSummary of New Unified API:"
|
|
113
|
+
puts '- Single Takagi::Client class for all protocols'
|
|
114
|
+
puts '- Auto-detects protocol from URI scheme (coap:// = UDP, coap+tcp:// = TCP)'
|
|
115
|
+
puts '- Explicit protocol selection with protocol: parameter'
|
|
116
|
+
puts '- Block-based initialization for automatic cleanup'
|
|
117
|
+
puts '- Consistent API across UDP and TCP transports'
|
|
118
|
+
puts '- Thread-safe with proper resource cleanup'
|