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/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 "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
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'