mqtt-core 0.0.1.ci.release
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 +7 -0
- data/lib/mqtt/core/client/acknowledgement.rb +31 -0
- data/lib/mqtt/core/client/client_id_generator.rb +19 -0
- data/lib/mqtt/core/client/connection.rb +193 -0
- data/lib/mqtt/core/client/enumerable_subscription.rb +224 -0
- data/lib/mqtt/core/client/filesystem_session_store.rb +197 -0
- data/lib/mqtt/core/client/memory_session_store.rb +84 -0
- data/lib/mqtt/core/client/qos0_session_store.rb +56 -0
- data/lib/mqtt/core/client/qos2_session_store.rb +45 -0
- data/lib/mqtt/core/client/qos_tracker.rb +119 -0
- data/lib/mqtt/core/client/retry_strategy.rb +63 -0
- data/lib/mqtt/core/client/session.rb +195 -0
- data/lib/mqtt/core/client/session_store.rb +128 -0
- data/lib/mqtt/core/client/socket_factory.rb +268 -0
- data/lib/mqtt/core/client/subscription.rb +109 -0
- data/lib/mqtt/core/client/uri.rb +77 -0
- data/lib/mqtt/core/client.rb +700 -0
- data/lib/mqtt/core/packet/connect.rb +39 -0
- data/lib/mqtt/core/packet/publish.rb +45 -0
- data/lib/mqtt/core/packet/subscribe.rb +190 -0
- data/lib/mqtt/core/packet/unsubscribe.rb +21 -0
- data/lib/mqtt/core/packet.rb +168 -0
- data/lib/mqtt/core/type/binary.rb +35 -0
- data/lib/mqtt/core/type/bit_flags.rb +89 -0
- data/lib/mqtt/core/type/boolean_byte.rb +48 -0
- data/lib/mqtt/core/type/fixed_int.rb +43 -0
- data/lib/mqtt/core/type/list.rb +41 -0
- data/lib/mqtt/core/type/password.rb +36 -0
- data/lib/mqtt/core/type/properties.rb +124 -0
- data/lib/mqtt/core/type/reason_codes.rb +60 -0
- data/lib/mqtt/core/type/remaining.rb +30 -0
- data/lib/mqtt/core/type/shape.rb +177 -0
- data/lib/mqtt/core/type/sub_type.rb +34 -0
- data/lib/mqtt/core/type/utf8_string.rb +39 -0
- data/lib/mqtt/core/type/utf8_string_pair.rb +29 -0
- data/lib/mqtt/core/type/var_int.rb +56 -0
- data/lib/mqtt/core/version.rb +10 -0
- data/lib/mqtt/core.rb +5 -0
- data/lib/mqtt/errors.rb +39 -0
- data/lib/mqtt/logger.rb +92 -0
- data/lib/mqtt/open.rb +239 -0
- data/lib/mqtt/options.rb +59 -0
- data/lib/mqtt/version.rb +6 -0
- data/lib/mqtt.rb +3 -0
- data/lib/patches/openssl.rb +19 -0
- metadata +98 -0
data/lib/mqtt/logger.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sugar for logging
|
|
4
|
+
require 'logger'
|
|
5
|
+
require 'delegate'
|
|
6
|
+
|
|
7
|
+
module MQTT
|
|
8
|
+
# The MQTT Logger
|
|
9
|
+
module Logger
|
|
10
|
+
# @!visibility private
|
|
11
|
+
# Adds classname info as progname to each log message
|
|
12
|
+
class InstanceLogger < SimpleDelegator
|
|
13
|
+
def initialize(obj)
|
|
14
|
+
@obj = obj
|
|
15
|
+
super(Logger.log)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
%i[debug info warn error fatal].each do |lvl|
|
|
19
|
+
lvl_check = :"#{lvl}?"
|
|
20
|
+
lvl_const = ::Logger.const_get(lvl.upcase)
|
|
21
|
+
define_method(lvl) do |message = nil, &block|
|
|
22
|
+
next unless __get_obj__.public_send(lvl_check)
|
|
23
|
+
|
|
24
|
+
add(lvl_const, message, &block)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add(severity, message = nil, &block)
|
|
29
|
+
if block
|
|
30
|
+
__get_obj__.add(severity, nil, @obj.log_name) { convert_message(block.call) }
|
|
31
|
+
else
|
|
32
|
+
__get_obj__.add(severity, convert_message(message), @obj.log_name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def __get_obj__
|
|
39
|
+
Logger.log
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def convert_message(msg)
|
|
43
|
+
case msg
|
|
44
|
+
when Exception
|
|
45
|
+
["#{msg.class.name}: #{msg.message}", *(__get_obj__.debug? ? msg.backtrace : [])].join("\n")
|
|
46
|
+
else
|
|
47
|
+
msg
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# @!visibility private
|
|
54
|
+
def configure(file: nil, shift_age: nil, shift_size: nil, level: nil)
|
|
55
|
+
shift_age = Integer(shift_age) if shift_age
|
|
56
|
+
shift_size = Integer(shift_size) if shift_size
|
|
57
|
+
|
|
58
|
+
device = [file, shift_age, shift_size].compact
|
|
59
|
+
if device.any?
|
|
60
|
+
send(:log=, *device, level: (level || defined?(@log) ? @log.level : ::Logger::INFO))
|
|
61
|
+
elsif level
|
|
62
|
+
log.level = level
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @!visibility private
|
|
67
|
+
# Set the log device
|
|
68
|
+
def log=(*device, level: defined?(@log) ? @log.level : ::Logger::INFO)
|
|
69
|
+
@log = ::Logger.new(*device, level:)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @!visibility private
|
|
73
|
+
# The actual logger
|
|
74
|
+
# @return [::Logger]
|
|
75
|
+
# @example
|
|
76
|
+
# MQTT::Logger.log.warn! # set level to WARN
|
|
77
|
+
def log
|
|
78
|
+
@log ||= send(:log=, $stdout).tap { $stdout.sync = true }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @!visibility private
|
|
83
|
+
def log
|
|
84
|
+
@log ||= InstanceLogger.new(self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @!visibility private
|
|
88
|
+
def log_name
|
|
89
|
+
self.class.name
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/mqtt/open.rb
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'core/client'
|
|
4
|
+
require_relative 'options'
|
|
5
|
+
|
|
6
|
+
# MQTT top level namespace
|
|
7
|
+
module MQTT
|
|
8
|
+
class << self
|
|
9
|
+
include Options
|
|
10
|
+
|
|
11
|
+
# Construct and Configure an MQTT client
|
|
12
|
+
#
|
|
13
|
+
# If a block is provided, yields the client and ensures disconnection.
|
|
14
|
+
# Otherwise, the client is returned directly.
|
|
15
|
+
#
|
|
16
|
+
# In either case the client is provided in `:configure` state and not yet connected to the broker.
|
|
17
|
+
#
|
|
18
|
+
# @note All scalar-valued options (String, Integer, Float, Boolean) can be provided as URI query parameters
|
|
19
|
+
# unless `ignore_uri_params: true` is set. URI parameters override explicit keyword arguments.
|
|
20
|
+
#
|
|
21
|
+
# @overload open(*io_args, **client_opts)
|
|
22
|
+
# @param io_args see {Core::Client::SocketFactory}. Typically an `mqtt[s]://` or `unix://` URI
|
|
23
|
+
# @param [Hash<Symbol>] client_opts
|
|
24
|
+
# @option client_opts [Boolean] ignore_uri_params (false)
|
|
25
|
+
# When true, URI query parameters are ignored. Use where URI input is untrusted.
|
|
26
|
+
# @option client_opts [Hash<Symbol>] **socket_factory options passed to {Core::Client::SocketFactory}
|
|
27
|
+
#
|
|
28
|
+
# - `host`, `port`, `local_addr`, `local_port`, `scheme`, `connect_timeout`, `ssl_(.*)`
|
|
29
|
+
# @option client_opts [Hash<Symbol>] **logger options passed to {MQTT::Logger.configure}
|
|
30
|
+
#
|
|
31
|
+
# - `log_(.*)`
|
|
32
|
+
# @option client_opts [Integer] protocol_version ([5, 3]) MQTT protocol major version
|
|
33
|
+
#
|
|
34
|
+
# - 5 requires `mqtt/v5` gem
|
|
35
|
+
# - 3 requires `mqtt/v3` gem.
|
|
36
|
+
# - If not set, v5 is preferred over v3 based on the ability to load the requisite gem.
|
|
37
|
+
# @option client_opts [String, nil] client_id ('') Session identifier
|
|
38
|
+
#
|
|
39
|
+
# - Default empty string implies anonymous or server-assigned ids
|
|
40
|
+
# - Set to nil to force generation of a random id if the broker does not support anonymous ids
|
|
41
|
+
# - {Core::Client::FilesystemSessionStore} requires a fixed, non-empty client_id
|
|
42
|
+
# @option client_opts [Integer, nil] session_expiry_interval Duration in seconds before the session
|
|
43
|
+
# expires after disconnect
|
|
44
|
+
#
|
|
45
|
+
# - Set to nil to use maximum (130+ years) expiry
|
|
46
|
+
# @option client_opts [String, Pathname] session_base_dir Base directory for
|
|
47
|
+
# {Core::Client::FilesystemSessionStore}
|
|
48
|
+
# @option client_opts [Core::Client::SessionStore, Class, Proc] session_store SessionStore instance, Class,
|
|
49
|
+
# or factory
|
|
50
|
+
#
|
|
51
|
+
# - Class/Proc is called with `client_id` and `session_(.*)` options
|
|
52
|
+
# - If not set
|
|
53
|
+
# - with `:session_base_dir` a {Core::Client::FilesystemSessionStore} is constructed
|
|
54
|
+
# - with `:session_expiry_interval` a {Core::Client::MemorySessionStore} is constructed
|
|
55
|
+
# - defaults to constructing a {Core::Client::Qos0SessionStore}
|
|
56
|
+
#
|
|
57
|
+
# @option client_opts [Core::Client::RetryStrategy, Class, Proc, Boolean] retry_strategy RetryStrategy instance,
|
|
58
|
+
# Class, or factory. Determines how to retry after connection dropouts.
|
|
59
|
+
#
|
|
60
|
+
# - Class/Proc is called with other `retry_(.*)` options
|
|
61
|
+
# - Explicitly false or nil disables retries
|
|
62
|
+
# - If not set
|
|
63
|
+
# - If other `retry_(.*)` options are present, they are used to construct a {Core::Client::RetryStrategy}
|
|
64
|
+
# - If the `:session_store` option resolves to only support QoS0, then retries are disabled
|
|
65
|
+
# - Otherwise, a {Core::Client::RetryStrategy} is constructed with default settings
|
|
66
|
+
# @option client_opts [Integer] keep_alive (60)
|
|
67
|
+
# Duration in seconds between PING packets sent to the broker to keep the connection alive.
|
|
68
|
+
# @option client_opts [Hash<Symbol>] **topic_alias options to build a {V5::TopicAlias::Manager} (V5 only)
|
|
69
|
+
#
|
|
70
|
+
# - `topic_aliases` TopicAlias::Manager instance, Class, or factory. (default {V5::TopicAlias::Manager})
|
|
71
|
+
# - `policy` TopicAlias::Policy instance, Class, or factory. (default {V5::TopicAlias::LRUPolicy})
|
|
72
|
+
# - `send_maximum` Integer maximum number of topic aliases to send to the broker.
|
|
73
|
+
# Default is the broker limit if a policy is set, otherwise 0, effectively disabling outgoing topic
|
|
74
|
+
# aliases.
|
|
75
|
+
#
|
|
76
|
+
# @option client_opts [Hash<Symbol>] **connect other CONNECT packet options
|
|
77
|
+
# @yield [client] Yields the configured client for use within the block
|
|
78
|
+
# @yieldparam client [Core::Client] the MQTT client instance {V5::Client} or {V3::Client}
|
|
79
|
+
# @return [Core::Client] the client if no block is given
|
|
80
|
+
# @return [void] if a block is given
|
|
81
|
+
# @raise [LoadError] if no suitable MQTT gem can be found
|
|
82
|
+
# @example Default usage - entirely configured via URI from environment variable
|
|
83
|
+
# # ENV['MQTT_SERVER'] => 'mqtt://localhost'
|
|
84
|
+
# MQTT.open do |client|
|
|
85
|
+
# client.connect(will_topic: 'status', will_payload: 'offline')
|
|
86
|
+
# client.publish(topic, payload)
|
|
87
|
+
# end
|
|
88
|
+
# @example Untrusted URI input, only directly set options are used
|
|
89
|
+
# MQTT.open(untrusted_uri, ignore_uri_params: true, ssl_min_version: :TLSv1_2) do |client|
|
|
90
|
+
# client.connect
|
|
91
|
+
# #...
|
|
92
|
+
# end
|
|
93
|
+
# @example Force v5 protocol
|
|
94
|
+
# MQTT.open(mqtt_uri, protocol_version: 5) do |client|
|
|
95
|
+
# client.connect
|
|
96
|
+
# # ...
|
|
97
|
+
# end
|
|
98
|
+
# @example Need Qos1/2 guarantees only as long as the process is running
|
|
99
|
+
# MQTT.open(mqtt_uri, session_expiry_interval: nil) do |client|
|
|
100
|
+
# client.session_store # => Core::Client::MemorySessionStore
|
|
101
|
+
# client.publish(topic, payload, qos: 2)
|
|
102
|
+
# # ...
|
|
103
|
+
# end
|
|
104
|
+
# @example Full Qos1/2 guarantees including over process restarts within one day
|
|
105
|
+
# mqtt_uri = "mqtt://host.example.com/?client_id=#{ENV['HOSTNAME']}"
|
|
106
|
+
# MQTT.open(mqtt_uri, session_base_dir: '/data/mqtt', session_expiry_interval: 86400) do |client|
|
|
107
|
+
# client.session_store # => Core::Client::FilesystemSessionStore
|
|
108
|
+
# client.on_birth do
|
|
109
|
+
# client.subscribe(*topics, max_qos: 2).async { |topic, payload| process(topic, payload) }
|
|
110
|
+
# end
|
|
111
|
+
# sleep # until process terminates
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
def open(*io_args, async: false, **client_opts, &)
|
|
115
|
+
# SocketFactory will claim its keyword options, client_opts is left with the remaining options
|
|
116
|
+
sf = Core::Client::SocketFactory.create(*io_args, options: client_opts)
|
|
117
|
+
client_opts.merge!(sf.query_params) if sf.respond_to?(:query_params)
|
|
118
|
+
MQTT::Logger.configure(**slice_opts!(client_opts, /^log_(.*)$/))
|
|
119
|
+
|
|
120
|
+
class_opts = { async: async, protocol_version: client_opts.delete(:protocol_version) || %w[5 3] }
|
|
121
|
+
client_class = client_class(**class_opts)
|
|
122
|
+
|
|
123
|
+
session_store = session_store(**slice_opts!(client_opts, :client_id, :session_store, /^session_(.*)$/))
|
|
124
|
+
|
|
125
|
+
open_opts = {
|
|
126
|
+
**retry_strategy(max_qos: session_store.max_qos, **slice_opts!(client_opts, :retry_strategy, /^retry_(.*)$/)),
|
|
127
|
+
**slice_opts!(client_opts, :keep_alive)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
v5_specific_options(client_class.protocol_version, client_opts, open_opts)
|
|
131
|
+
|
|
132
|
+
client_class.open(sf, session_store:, **client_opts, **open_opts, &)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Utility to test broker connectivity
|
|
136
|
+
#
|
|
137
|
+
# Connect to the broker, outputting status information
|
|
138
|
+
# @see open
|
|
139
|
+
def test(*io_args, **client_opts)
|
|
140
|
+
self.open(*io_args, **client_opts) do |client|
|
|
141
|
+
client.configure_retry(false)
|
|
142
|
+
puts "Client: #{client}"
|
|
143
|
+
puts " Session: #{client.session_store}"
|
|
144
|
+
client.connect
|
|
145
|
+
puts " Status: #{client.status}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# As per {open} but returns an Async client that uses Fibers for concurrency
|
|
150
|
+
#
|
|
151
|
+
# If not using the block form, {Core::Client#connect} and later methods must be called within the reactor.
|
|
152
|
+
# @example Async client usage
|
|
153
|
+
# require 'async'
|
|
154
|
+
# require 'mqtt/core'
|
|
155
|
+
#
|
|
156
|
+
# client = MQTT.async_open('localhost')
|
|
157
|
+
# Sync do
|
|
158
|
+
# client.connect
|
|
159
|
+
# client.publish(topic, payload)
|
|
160
|
+
# end
|
|
161
|
+
# @yield [client] Yields the configured client for use within the block
|
|
162
|
+
# @yieldparam client [Core::Client] the MQTT client instance {V5::Async::Client} or {V3::Async::Client}
|
|
163
|
+
# @return [Core::Client] the client if no block is given
|
|
164
|
+
# @return [void] if a block is given
|
|
165
|
+
def async_open(*io_args, **client_opts, &)
|
|
166
|
+
self.open(*io_args, async: true, **client_opts, &)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def client_class(protocol_version:, async:)
|
|
172
|
+
protocol_version = [protocol_version] unless protocol_version.is_a?(Array)
|
|
173
|
+
protocol_version.map(&:to_s).each do |v|
|
|
174
|
+
raise ArgumentError, "Invalid mqtt-version #{v}" unless %w[3 5 31 3.1 50 5.0 3.1.1 311].include?(v)
|
|
175
|
+
|
|
176
|
+
v = v[0]
|
|
177
|
+
require async ? "mqtt/v#{v}/async/client" : "mqtt/v#{v}"
|
|
178
|
+
return Class.const_get("MQTT::V#{v}::#{'Async::' if async}Client")
|
|
179
|
+
rescue LoadError => _e
|
|
180
|
+
# warn e
|
|
181
|
+
end
|
|
182
|
+
raise LoadError, "Could not find MQTT gem for protocol versions: #{protocol_version}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def session_store(session_store: nil, **opts)
|
|
186
|
+
return session_store.call(**opts) if session_store.respond_to?(:call)
|
|
187
|
+
return session_store.new(**opts) if session_store.is_a?(Class)
|
|
188
|
+
return session_store if session_store # expecting something that quacks like Core::Client::SessionStore
|
|
189
|
+
|
|
190
|
+
return Core::Client.file_store(**opts) if opts.key?(:base_dir)
|
|
191
|
+
return Core::Client.memory_store(**opts) if opts.key?(:expiry_interval)
|
|
192
|
+
|
|
193
|
+
Core::Client.qos0_store(**opts)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @return [RetryStrategy] URI parameters (prefixed 'retry_') override provided defaults
|
|
197
|
+
# @see open
|
|
198
|
+
def retry_strategy(max_qos:, retry_strategy: max_qos.positive? || :__not_set__, **options)
|
|
199
|
+
return {} if retry_strategy == :__not_set__ && options.empty?
|
|
200
|
+
return { retry_strategy: retry_strategy.call(**options) } if retry_strategy.respond_to?(:call)
|
|
201
|
+
return { retry_strategy: retry_strategy.new(**options) } if retry_strategy.is_a?(Class)
|
|
202
|
+
return { retry_strategy: false } if %w[false 0 no n nil].include?(retry_strategy.to_s.downcase)
|
|
203
|
+
|
|
204
|
+
{ retry_strategy: Core::Client.retry_strategy(**options) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# extract from client_opts into open_opts
|
|
208
|
+
def v5_specific_options(protocol_version, client_opts, open_opts)
|
|
209
|
+
ta_keys = [:topic_aliases, :topic_alias_maximum, /^topic_alias_(.*)$/]
|
|
210
|
+
v5_opts(protocol_version, client_opts, open_opts, *ta_keys) { |**ta_opts| topic_alias_opts(**ta_opts) }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def v5_opts(protocol_version, client_opts, open_opts, *slice_keys, &block)
|
|
214
|
+
v5_opts = slice_opts!(client_opts, *slice_keys)
|
|
215
|
+
if protocol_version == 5
|
|
216
|
+
open_opts.merge!(block.call(**v5_opts))
|
|
217
|
+
else
|
|
218
|
+
MQTT::Logger.log.warn "Ignoring #{slice_keys.last} options for protocol version #{protocol_version}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def topic_alias_opts(
|
|
223
|
+
topic_aliases: V5::TopicAlias::Manager, topic_alias_maximum: nil,
|
|
224
|
+
policy: nil, send_maximum: :__not_set__, **
|
|
225
|
+
)
|
|
226
|
+
# Use broker limit if we have a policy but no send limit
|
|
227
|
+
send_maximum = nil if send_maximum == :__not_set__
|
|
228
|
+
send_maximum = coerce_integer(send_maximum:) if send_maximum
|
|
229
|
+
|
|
230
|
+
policy = policy.call(**) if policy.respond_to?(:call)
|
|
231
|
+
policy = policy.new(**) if policy.is_a?(Class)
|
|
232
|
+
|
|
233
|
+
topic_aliases = topic_aliases.call(policy:, send_maximum:, **) if topic_aliases.respond_to?(:call)
|
|
234
|
+
topic_aliases = topic_aliases.new(policy:, send_maximum:, **) if topic_aliases.is_a?(Class)
|
|
235
|
+
|
|
236
|
+
{ topic_aliases: topic_aliases, topic_alias_maximum: }.compact
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
data/lib/mqtt/options.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
# @!visibility private
|
|
5
|
+
module Options
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Slice out a set of options, removing them from the original hash
|
|
9
|
+
# @param [Hash] opts
|
|
10
|
+
# @param [Array<Symbol,Regexp>] keys
|
|
11
|
+
# - Symbol: exact match
|
|
12
|
+
# - Regexp: match against key, using first matching group to name the option (eg remove prefix/suffix)
|
|
13
|
+
# @return [Hash]
|
|
14
|
+
def slice_opts!(opts, *keys) # rubocop:disable Metrics/AbcSize
|
|
15
|
+
{}.tap do |slice|
|
|
16
|
+
opts.delete_if do |o, v|
|
|
17
|
+
keys.any? do |k|
|
|
18
|
+
if k.is_a?(Regexp)
|
|
19
|
+
k.match(o).tap { slice[(it[1] || o).to_sym] = v if it }
|
|
20
|
+
else
|
|
21
|
+
(k.to_sym == o).tap { slice[o] = v if it }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
slice.each { |k, v| slice[k] = yield(k, v) } if block_given?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def coerce_boolean(_key, value)
|
|
30
|
+
return value if [true, false, nil].include?(value)
|
|
31
|
+
return false if value.to_s.match?(/^(false|f|no|n|0)$/i)
|
|
32
|
+
|
|
33
|
+
!!value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def coerce_integer(key = nil, value = nil, **opts)
|
|
37
|
+
key, value = opts.first if opts.size == 1
|
|
38
|
+
coerce(key, value, type: :Integer)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def coerce_float(key, value)
|
|
42
|
+
coerce(key, value, type: :Float)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @!visibility private
|
|
46
|
+
def coerce(key, value, type: :Integer)
|
|
47
|
+
return value unless value
|
|
48
|
+
|
|
49
|
+
case type
|
|
50
|
+
when :Integer
|
|
51
|
+
Integer(value)
|
|
52
|
+
when :Float
|
|
53
|
+
Float(value)
|
|
54
|
+
end
|
|
55
|
+
rescue ArgumentError => e
|
|
56
|
+
raise ArgumentError, "#{key}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/mqtt/version.rb
ADDED
data/lib/mqtt.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
if Gem::Version.new(OpenSSL::VERSION) < Gem::Version.new('3.3.0')
|
|
6
|
+
module OpenSSL
|
|
7
|
+
module SSL
|
|
8
|
+
# Socket patches
|
|
9
|
+
class SSLSocket
|
|
10
|
+
# https://github.com/ruby/openssl/pull/771
|
|
11
|
+
unless method_defined?(:readbyte)
|
|
12
|
+
def readbyte
|
|
13
|
+
getbyte or raise EOFError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mqtt-core
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1.ci.release
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Grant Gardner
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: concurrent_monitor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: ''
|
|
27
|
+
email:
|
|
28
|
+
- grant@lastweekend.com.au
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- lib/mqtt.rb
|
|
34
|
+
- lib/mqtt/core.rb
|
|
35
|
+
- lib/mqtt/core/client.rb
|
|
36
|
+
- lib/mqtt/core/client/acknowledgement.rb
|
|
37
|
+
- lib/mqtt/core/client/client_id_generator.rb
|
|
38
|
+
- lib/mqtt/core/client/connection.rb
|
|
39
|
+
- lib/mqtt/core/client/enumerable_subscription.rb
|
|
40
|
+
- lib/mqtt/core/client/filesystem_session_store.rb
|
|
41
|
+
- lib/mqtt/core/client/memory_session_store.rb
|
|
42
|
+
- lib/mqtt/core/client/qos0_session_store.rb
|
|
43
|
+
- lib/mqtt/core/client/qos2_session_store.rb
|
|
44
|
+
- lib/mqtt/core/client/qos_tracker.rb
|
|
45
|
+
- lib/mqtt/core/client/retry_strategy.rb
|
|
46
|
+
- lib/mqtt/core/client/session.rb
|
|
47
|
+
- lib/mqtt/core/client/session_store.rb
|
|
48
|
+
- lib/mqtt/core/client/socket_factory.rb
|
|
49
|
+
- lib/mqtt/core/client/subscription.rb
|
|
50
|
+
- lib/mqtt/core/client/uri.rb
|
|
51
|
+
- lib/mqtt/core/packet.rb
|
|
52
|
+
- lib/mqtt/core/packet/connect.rb
|
|
53
|
+
- lib/mqtt/core/packet/publish.rb
|
|
54
|
+
- lib/mqtt/core/packet/subscribe.rb
|
|
55
|
+
- lib/mqtt/core/packet/unsubscribe.rb
|
|
56
|
+
- lib/mqtt/core/type/binary.rb
|
|
57
|
+
- lib/mqtt/core/type/bit_flags.rb
|
|
58
|
+
- lib/mqtt/core/type/boolean_byte.rb
|
|
59
|
+
- lib/mqtt/core/type/fixed_int.rb
|
|
60
|
+
- lib/mqtt/core/type/list.rb
|
|
61
|
+
- lib/mqtt/core/type/password.rb
|
|
62
|
+
- lib/mqtt/core/type/properties.rb
|
|
63
|
+
- lib/mqtt/core/type/reason_codes.rb
|
|
64
|
+
- lib/mqtt/core/type/remaining.rb
|
|
65
|
+
- lib/mqtt/core/type/shape.rb
|
|
66
|
+
- lib/mqtt/core/type/sub_type.rb
|
|
67
|
+
- lib/mqtt/core/type/utf8_string.rb
|
|
68
|
+
- lib/mqtt/core/type/utf8_string_pair.rb
|
|
69
|
+
- lib/mqtt/core/type/var_int.rb
|
|
70
|
+
- lib/mqtt/core/version.rb
|
|
71
|
+
- lib/mqtt/errors.rb
|
|
72
|
+
- lib/mqtt/logger.rb
|
|
73
|
+
- lib/mqtt/open.rb
|
|
74
|
+
- lib/mqtt/options.rb
|
|
75
|
+
- lib/mqtt/version.rb
|
|
76
|
+
- lib/patches/openssl.rb
|
|
77
|
+
licenses:
|
|
78
|
+
- MIT
|
|
79
|
+
metadata:
|
|
80
|
+
rubygems_mfa_required: 'true'
|
|
81
|
+
rdoc_options: []
|
|
82
|
+
require_paths:
|
|
83
|
+
- lib
|
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.4'
|
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
requirements: []
|
|
95
|
+
rubygems_version: 3.6.9
|
|
96
|
+
specification_version: 4
|
|
97
|
+
summary: Ruby MQTT Core
|
|
98
|
+
test_files: []
|