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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ulms_client.rb +230 -0
  3. metadata +58 -0
@@ -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
@@ -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: []