ulms_client 0.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 +7 -0
- data/lib/ulms_client.rb +230 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9beb3f2355f9d609ea0d071d329c13be751f1b3a
|
4
|
+
data.tar.gz: c37801f763f2e5533d156ee2f603993d29ac4036
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8acce0470a05127cc3bed388770c0dc1f0f150a877c332593afe695bc34fe43db5d153d9e5d69fe90807d79ae1f3abe1dbf1eb98145effd1b82f33a32c11517f
|
7
|
+
data.tar.gz: ead0e6b6b03889b421ebbc61cc40e98c15ddc3aad341c0d0657eab9082f6d4ae580a50960e6f42712ab640c75bf9c092e510fcf111d84da0815b700649239d33
|
data/lib/ulms_client.rb
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'logger'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'timeout'
|
5
|
+
require 'mqtt'
|
6
|
+
|
7
|
+
LOG = Logger.new(STDOUT)
|
8
|
+
LOG.level = Logger::INFO;
|
9
|
+
|
10
|
+
DEFAULT_TIMEOUT = 5
|
11
|
+
|
12
|
+
###############################################################################
|
13
|
+
|
14
|
+
class AssertionError < StandardError; end
|
15
|
+
|
16
|
+
class Account
|
17
|
+
attr_reader :label, :audience
|
18
|
+
|
19
|
+
def initialize(label, audience)
|
20
|
+
@label = label
|
21
|
+
@audience = audience
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"#{@label}.#{@audience}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Agent
|
30
|
+
attr_reader :label, :account
|
31
|
+
|
32
|
+
def initialize(label, account)
|
33
|
+
@label = label
|
34
|
+
@account = account
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"#{@label}.#{@account}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Client
|
43
|
+
attr_reader :version, :mode, :agent
|
44
|
+
|
45
|
+
def initialize(version:, mode:, agent:)
|
46
|
+
@version = version
|
47
|
+
@mode = mode
|
48
|
+
@agent = agent
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"#{@version}/#{@mode}/#{@agent}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Connection
|
57
|
+
OPTIONS = [:username, :password, :clean_session, :keep_alive]
|
58
|
+
|
59
|
+
def initialize(host:, port:, client:, **kwargs)
|
60
|
+
@client = client
|
61
|
+
|
62
|
+
@mqtt = MQTT::Client.new
|
63
|
+
@mqtt.host = host
|
64
|
+
@mqtt.port = port
|
65
|
+
@mqtt.client_id = client.to_s
|
66
|
+
|
67
|
+
OPTIONS.each do |option|
|
68
|
+
@mqtt.send("#{option}=", kwargs[option]) if kwargs[option] != nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Establish the connection.
|
73
|
+
def connect
|
74
|
+
@mqtt.connect
|
75
|
+
LOG.info("#{@client} connected")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Disconnect from the broker.
|
79
|
+
def disconnect
|
80
|
+
@mqtt.disconnect
|
81
|
+
LOG.info("#{@client} disconnected")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Publish a message to the `topic`.
|
85
|
+
#
|
86
|
+
# Options:
|
87
|
+
# - `payload`: An object that will be dumped into JSON as the message payload (required).
|
88
|
+
# - `properties`: MQTT publish properties hash.
|
89
|
+
# - `retain`: A boolean indicating whether the messages should be retained.
|
90
|
+
# - `qos`: An integer 0..2 that sets the QoS.
|
91
|
+
def publish(topic, payload:, properties: {}, retain: false, qos: 0)
|
92
|
+
envelope = {
|
93
|
+
payload: JSON.dump(payload),
|
94
|
+
properties: properties
|
95
|
+
}
|
96
|
+
|
97
|
+
@mqtt.publish(topic, JSON.dump(envelope), retain, qos)
|
98
|
+
|
99
|
+
LOG.info <<~EOF
|
100
|
+
#{@client.agent} published to #{topic} (q#{qos}, r#{retain ? 1 : 0}):
|
101
|
+
Payload: #{JSON.pretty_generate(payload)}
|
102
|
+
Properties: #{JSON.pretty_generate(properties)}
|
103
|
+
EOF
|
104
|
+
end
|
105
|
+
|
106
|
+
# Subscribe to the `topic`.
|
107
|
+
#
|
108
|
+
# Options:
|
109
|
+
# - `qos`: Subscriptions QoS. An interger 0..2.
|
110
|
+
def subscribe(topic, qos: 0)
|
111
|
+
@mqtt.subscribe([topic, qos])
|
112
|
+
LOG.info("#{@client.agent} subscribed to #{topic} (q#{qos})")
|
113
|
+
end
|
114
|
+
|
115
|
+
# Waits for an incoming message.
|
116
|
+
# If a block is given it passes the received message to the block.
|
117
|
+
# If the block returns falsey value it waits for the next one and so on.
|
118
|
+
# Returns the received message.
|
119
|
+
# Raises if `timeout` is over.
|
120
|
+
def receive(timeout=DEFAULT_TIMEOUT)
|
121
|
+
Timeout::timeout(timeout, nil, "Timed out waiting for the message") do
|
122
|
+
loop do
|
123
|
+
topic, json = @mqtt.get
|
124
|
+
envelope = JSON.load(json)
|
125
|
+
payload = JSON.load(envelope['payload'])
|
126
|
+
message = IncomingMessage.new(topic, payload, envelope['properties'])
|
127
|
+
|
128
|
+
LOG.info <<~EOF
|
129
|
+
#{@client.agent} received a message from topic #{topic}:
|
130
|
+
Payload: #{JSON.pretty_generate(message.payload)}
|
131
|
+
Properties: #{JSON.pretty_generate(message.properties)}
|
132
|
+
EOF
|
133
|
+
|
134
|
+
return message unless block_given?
|
135
|
+
|
136
|
+
if yield(message)
|
137
|
+
LOG.info "The message matched the given predicate"
|
138
|
+
return message
|
139
|
+
else
|
140
|
+
LOG.info "The message didn't match the given predicate. Waiting for the next one."
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# A high-level method that makes a request and waits for the response on it.
|
147
|
+
#
|
148
|
+
# Options:
|
149
|
+
# - `to`: the destination service `Account` (required).
|
150
|
+
# - `payload`: the publish message payload (required).
|
151
|
+
# - `properties`: additional MQTT properties hash.
|
152
|
+
# - `qos`: Publish QoS. An integer 0..2.
|
153
|
+
# - `timeout`: Timeout for the response awaiting.
|
154
|
+
def make_request(method, to:, payload:, properties: {}, qos: 0, timeout: DEFAULT_TIMEOUT)
|
155
|
+
correlation_data = SecureRandom.hex
|
156
|
+
|
157
|
+
properties.merge!({
|
158
|
+
type: 'request',
|
159
|
+
method: method,
|
160
|
+
correlation_data: correlation_data,
|
161
|
+
response_topic: "agents/#{@client.agent}/api/v1/in/#{to}"
|
162
|
+
})
|
163
|
+
|
164
|
+
topic = "agents/#{@client.agent}/api/v1/out/#{to}"
|
165
|
+
publish(topic, payload: payload, properties: properties, qos: qos)
|
166
|
+
|
167
|
+
receive(timeout) do |msg|
|
168
|
+
msg.properties['type'] == 'response' &&
|
169
|
+
msg.properties['correlation_data'] == correlation_data
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class IncomingMessage
|
175
|
+
attr_reader :topic, :payload, :properties
|
176
|
+
|
177
|
+
def initialize(topic, payload, properties)
|
178
|
+
@topic = topic
|
179
|
+
@payload = payload
|
180
|
+
@properties = properties
|
181
|
+
end
|
182
|
+
|
183
|
+
# A shortcut for payload fields. `msg['key']` is the same as `msg.payload['key']`.
|
184
|
+
def [](key)
|
185
|
+
@payload[key]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
###############################################################################
|
190
|
+
|
191
|
+
# Raises unless the given argument is truthy.
|
192
|
+
def assert(value)
|
193
|
+
raise AssertionError.new("Assertion failed") unless value
|
194
|
+
end
|
195
|
+
|
196
|
+
# Builds an `Agent` instance.
|
197
|
+
def agent(label, account)
|
198
|
+
Agent.new(label, account)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Builds an `Account` instance.
|
202
|
+
def account(label, audience)
|
203
|
+
Account.new(label, audience)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Builds a `Client` instance.
|
207
|
+
#
|
208
|
+
# Options:
|
209
|
+
# - `mode`: Connection mode (required). Available values: `agents`, `service-agents`, `bridge-agents`, `observer-agents`.
|
210
|
+
# - `version`: Always `v1` for now.
|
211
|
+
def client(agent, mode:, version: 'v1')
|
212
|
+
Client.new(version: version, mode: mode, agent: agent)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Connects to the broker and subscribes to the client's inbox topics.
|
216
|
+
#
|
217
|
+
# Options:
|
218
|
+
# - `host`: The broker's host (required).
|
219
|
+
# - `port`: The broker's TCP port for MQTT connections (required).
|
220
|
+
# - `client`: The `Client` object (required).
|
221
|
+
# - `username`: If the broker has authn enabled this requires any non-empty string.
|
222
|
+
# - `password`: If the broker has authn enalbed this requires the password for the `client`'s account.
|
223
|
+
# - `clean_session`: A boolean indicating whether the broker has to clean the previos session.
|
224
|
+
# - `keep_alive`: Keep alive time in seconds.
|
225
|
+
def connect(host: 'localhost', port: 1883, client:, **kwargs)
|
226
|
+
conn = Connection.new(host: host, port: port, client: client, **kwargs)
|
227
|
+
conn.connect
|
228
|
+
conn.subscribe("agents/#{client.agent}/api/v1/#")
|
229
|
+
conn
|
230
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ulms_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Timofey Martynov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mqtt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.5'
|
27
|
+
description:
|
28
|
+
email: t.martynov@talenttech.ru
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/ulms_client.rb
|
34
|
+
homepage: https://rubygems.org/gems/ulms_client
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 2.6.14.1
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: DSL for writing ULMS interaction scenarios
|
58
|
+
test_files: []
|