ulms_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []