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