ducts-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ba367c428158d0baa377345220b10c67538329d4e0167b1d6dce86a29d732a0
4
+ data.tar.gz: 72f19f903159044c7f88c04b8a60e45fdc64672c44c7583f82f4d81d3cc4b5e9
5
+ SHA512:
6
+ metadata.gz: 6e4e8e33b10ee10c684a9f09b0a0871f2cef598b8f3a80c88cfec56bbcd8a952790ad79079201994ac09f840faebc0cdff6121324d6d753b3c6669da3f6ad1c2
7
+ data.tar.gz: fa68f8a558a5a95826b0f0bcf3084d90582ccecfd072b4a8669b97645b43704065cf6c25f5d18f48f1712bb603c0d61ba9392950b7cb4688a9f0ea99a8c4d9bd
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+ Gemfile.lock
11
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.3
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at kenshiro.ueda@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ductsclient.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 12.0'
7
+ gem 'minitest', '~> 5.0'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Kenshiro Ueda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # Ducts-client
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ductsclient`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ducts-client'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ducts-client
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/iflb/ducts-client-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/iflb/ducts-client-ruby/CODE_OF_CONDUCT.md).
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
41
+
42
+ ## Code of Conduct
43
+
44
+ Everyone interacting in the Ducts-client project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/iflb/ducts-client-ruby/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ductsclient"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ require_relative 'lib/ducts/client/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'ducts-client'
5
+ spec.version = Ducts::Client::VERSION
6
+ spec.authors = ['Kenshiro Ueda']
7
+ spec.email = ['ueda@iflab.co.jp']
8
+
9
+ spec.summary = 'APIs of DUCTS for ruby clients.'
10
+ spec.homepage = 'https://www.iflab.co.jp/'
11
+ spec.license = 'MIT'
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
13
+ spec.metadata['homepage_uri'] = spec.homepage
14
+ spec.metadata['source_code_uri'] = 'https://github.com/iflb/ducts-client-ruby'
15
+
16
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'faye-websocket', '~> 0.11'
24
+ spec.add_development_dependency 'msgpack', '~> 1.4'
25
+ end
@@ -0,0 +1,306 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require_relative 'client/version'
4
+ require 'thread'
5
+ require 'timeout'
6
+ require 'uri'
7
+ require 'net/http'
8
+ require 'json'
9
+ require 'faye/websocket'
10
+ require 'faye/websocket/api/event'
11
+ require 'msgpack'
12
+ require 'eventmachine'
13
+
14
+ module Ducts
15
+ class Client
16
+ attr_reader :WSD, :EVENT, :connection_listener
17
+ attr_accessor :catchall_event_handler, :uncaught_event_handler, :event_error_handler
18
+
19
+ private
20
+ class DuctError < StandardError; end
21
+
22
+ class DuctCallTimeout < DuctError; end
23
+
24
+ class DuctEvent; end
25
+
26
+ class DuctConnectionEvent < DuctEvent
27
+ attr_reader :state, :source
28
+ def initialize(state, source)
29
+ super()
30
+ @state = state
31
+ @source = source
32
+ end
33
+ end
34
+
35
+ class DuctMessageEvent < DuctEvent
36
+ attr_reader :rid, :eid, :data
37
+ def initialize(event)
38
+ super()
39
+ rid, eid, data = MessagePack.unpack(event.data.map(&:chr).join)
40
+ @rid = rid
41
+ @eid = eid
42
+ @data = data
43
+ end
44
+ end
45
+
46
+ class DuctEventListener
47
+ def on(names, func)
48
+ names = [ names ] unless names.kind_of? Array
49
+ names.each do |name|
50
+ unless methods.include? name.to_sym
51
+ raise NoMethodError.new("[#{name}] in #{self.class}")
52
+ end
53
+ remove_method(name.to_sym)
54
+ define_singleton_method(name.to_sym, func)
55
+ end
56
+ end
57
+ end
58
+
59
+ class ConnectionEventListener < DuctEventListener
60
+ attr_writer :onopen, :onclose, :onerror, :onmessage
61
+ def onopen(event)
62
+ @onopen.call(event) if @onopen
63
+ end
64
+ def onclose(event)
65
+ @onclose.call(event) if @onclose
66
+ end
67
+ def onerror(event)
68
+ @onerror.call(event) if @onerror
69
+ end
70
+ def onmessage(event)
71
+ @onmessage.call(event) if @onmessage
72
+ end
73
+ end
74
+
75
+ module State
76
+ CLOSE = -1
77
+ OPEN_CONNECTING = Faye::WebSocket::API::CONNECTING
78
+ OPEN_CONNECTED = Faye::WebSocket::API::OPEN
79
+ OPEN_CLOSING = Faye::WebSocket::API::CLOSING
80
+ OPEN_CLOSED = Faye::WebSocket::API::CLOSED
81
+
82
+ def self.get_state_from_value(value)
83
+ (constants.map{ |x| const_get(x) }.include? value)? value : nil
84
+ end
85
+ end
86
+
87
+ module Message
88
+ PREFIX = '[Ducts]'
89
+
90
+ module Level
91
+ LOG = 0
92
+ WARNING = 1
93
+ ERROR = 2
94
+ end
95
+
96
+ def self.construct(level, raw_message)
97
+ case level
98
+ when Level::LOG
99
+ PREFIX + '[LOG] ' + raw_message
100
+ when Level::WARNING
101
+ PREFIX + '[WARNING] ' + raw_message
102
+ when Level::ERROR
103
+ PREFIX + '[ERROR] ' + raw_message
104
+ end
105
+ end
106
+ end
107
+
108
+ public
109
+ def initialize
110
+ @WSD = nil
111
+ @EVENT = nil
112
+ @connection_listener = ConnectionEventListener.new
113
+ @catchall_event_handler = lambda{ |rid, eid, data| }
114
+ @uncaught_event_handler = lambda{ |rid, eid, data| }
115
+ @event_error_handler = lambda{ |rid, eid, data, error|
116
+ warn Message.construct(Message::Level::ERROR, error.message)
117
+ }
118
+
119
+ @_last_rid = nil
120
+ @_ws = nil
121
+ @_waiting_completion = Hash.new
122
+ end
123
+
124
+ def next_rid
125
+ next_id = Time.now.to_i
126
+ if (!@_last_rid.nil? && (next_id <= @_last_rid))
127
+ next_id = @_last_rid + 1
128
+ end
129
+ @_last_rid = next_id
130
+ next_id
131
+ end
132
+
133
+ def open(wsd_url, uuid = nil, **params, &proc)
134
+ _open(wsd_url, uuid, **params, &proc)
135
+ end
136
+
137
+ def reconnect
138
+ _reconnect
139
+ end
140
+
141
+ def send(rid, eid, data)
142
+ _send(rid, eid, data)
143
+ end
144
+
145
+ def call(eid, data)
146
+ _call(eid, data)
147
+ end
148
+
149
+ def close
150
+ _close
151
+ end
152
+
153
+ def set_event_handler(event_id, &handler)
154
+ @_event_handler ||= {}
155
+ @_event_handler[event_id] = handler
156
+ end
157
+
158
+ def state
159
+ if @_ws
160
+ State.get_state_from_value(@_ws.ready_state)
161
+ else
162
+ State::CLOSE
163
+ end
164
+ end
165
+
166
+ private
167
+ def _reconnect(wsd)
168
+ if wsd
169
+ @WSD = wsd
170
+ @EVENT = @WSD['EVENT']
171
+ end
172
+ return if @_ws
173
+ Faye::WebSocket::Client.new(@WSD['websocket_url_reconnect']).tap do |ws|
174
+ ws.on(:open) do |event|
175
+ @_ws = ws
176
+ connection_event = DuctConnectionEvent.new('onopen', event)
177
+ _onreconnect(connection_event)
178
+ @connection_listener.onopen(connection_event)
179
+ end
180
+ ws.on(:close) do |event|
181
+ connection_event = DuctConnectionEvent.new('onclose', event)
182
+ @connection_listener.onclose(connection_event)
183
+ end
184
+ ws.on(:message) do |event|
185
+ message_event = DuctMessageEvent.new(event)
186
+ connection_event = DuctConnectionEvent.new('onmessage', message_event)
187
+ _onmessage(connection_event)
188
+ @connection_listener.onmessage(connection_event)
189
+ end
190
+ ws.on(:error) do |event|
191
+ connection_event = DuctConnectionEvent.new('onerror', event)
192
+ @connection_listener.onerror(connection_event)
193
+ end
194
+ end
195
+ end
196
+
197
+ def _open(wsd_url, uuid, **params)
198
+ return if @_ws
199
+ begin
200
+ query = '?uuid=' + (uuid || 'dummy') + params.map{ |k, v| "&#{k}=#{v}" }.join
201
+ uri = URI.parse(wsd_url + query)
202
+ https = Net::HTTP.new(uri.host, Net::HTTP.https_default_port).tap{ |h|
203
+ h.ssl_version = :SSLv23
204
+ h.use_ssl = true
205
+ h.verify_mode = OpenSSL::SSL::VERIFY_PEER
206
+ }
207
+ response = https.get(uri)
208
+ @WSD = JSON.parse(response.body)
209
+ @EVENT = @WSD['EVENT']
210
+ Faye::WebSocket::Client.new(@WSD['websocket_url']).tap do |ws|
211
+ ws.on(:open) do |event|
212
+ @_ws = ws
213
+ connection_event = DuctConnectionEvent.new('onopen', event)
214
+ _onopen(connection_event)
215
+ @connection_listener.onopen(connection_event)
216
+ end
217
+ ws.on(:message) do |event|
218
+ message_event = DuctMessageEvent.new(event)
219
+ connection_event = DuctConnectionEvent.new('onmessage', message_event)
220
+ _onmessage(connection_event)
221
+ @connection_listener.onmessage(connection_event)
222
+ end
223
+ ws.on(:error) do |event|
224
+ connection_event = DuctConnectionEvent.new('onerror', event)
225
+ @connection_listener.onerror(connection_event)
226
+ end
227
+ ws.on(:close) do |event|
228
+ connection_event = DuctConnectionEvent.new('onclose', event)
229
+ @connection_listener.onclose(connection_event)
230
+ end
231
+ end
232
+ rescue => error
233
+ @connection_listener.onerror(DuctConnectionEvent.new('onerror', error))
234
+ end
235
+ end
236
+
237
+ def _onopen(event)
238
+ @_send_timestamp = Time.now.to_f
239
+ @_time_offset = 0
240
+ @_time_latency = 0
241
+ @_time_count = 0
242
+ set_event_handler(@EVENT['ALIVE_MONITORING']) do |rid, eid, data|
243
+ client_received = Time.now.to_f
244
+ server_sent, server_received = data
245
+ client_sent = @_send_timestamp
246
+ new_offset = ((server_received - client_sent) - (client_received - server_sent)) / 2
247
+ new_latency = ((client_received - client_sent) - (server_sent - server_received)) / 2
248
+ @_time_offset = (@_time_offset * @_time_count + new_offset) / (@_time_count + 1)
249
+ @_time_latency = (@_time_latency * @_time_count + new_latency) / (@_time_count + 1)
250
+ @_time_count += 1
251
+ end
252
+ rid = next_rid
253
+ eid = @EVENT['ALIVE_MONITORING']
254
+ value = @_send_timestamp
255
+ send(rid, eid, value)
256
+ end
257
+
258
+ def _onreconnect(connection_event)
259
+ end
260
+
261
+ def _send(rid, eid, data)
262
+ msgpack = [ rid, eid, data ].to_msgpack
263
+ @_ws.send(msgpack.chars.map(&:ord))
264
+ rid
265
+ end
266
+
267
+ def _call(eid, data)
268
+ rid = next_rid
269
+ _send(rid, eid, data)
270
+ raise if @_waiting_completion.keys.include? rid
271
+ EM::Completion.new.tap { |completion| @_waiting_completion[rid] = completion }
272
+ end
273
+
274
+ def _close
275
+ begin
276
+ @_ws.close if @_ws
277
+ ensure
278
+ @_ws = nil
279
+ end
280
+ end
281
+
282
+ def _onmessage(connection_event)
283
+ begin
284
+ rid, eid, data = %i(rid eid data).map{ |name| connection_event.source.public_send(name) }
285
+ begin
286
+ @catchall_event_handler.call(rid, eid, data)
287
+ handle = @_event_handler[eid]
288
+ handle = @uncaught_event_handler unless handle
289
+ handle.call(rid, eid, data)
290
+ completion = @_waiting_completion.delete(rid)
291
+ if completion
292
+ if eid > 0
293
+ completion.succeed(data)
294
+ else
295
+ completion.fail(DuctError.execption(data))
296
+ end
297
+ end
298
+ rescue => error
299
+ @event_error_handler.call(rid, eid, data, error)
300
+ end
301
+ rescue => error
302
+ @event_error_handler.call(-1, -1, nil, error)
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,13 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ module Ducts
4
+ class Client
5
+ module Version
6
+ MAJOR = 0
7
+ MINOR = 1
8
+ PATCH = 0
9
+ end
10
+
11
+ VERSION = [ Version::MAJOR, Version::MINOR, Version::PATCH ].map(&:to_s).join('.')
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ require_relative 'ducts/client'
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ducts-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kenshiro Ueda
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-05-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faye-websocket
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: msgpack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ description:
42
+ email:
43
+ - ueda@iflab.co.jp
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".travis.yml"
50
+ - CODE_OF_CONDUCT.md
51
+ - Gemfile
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - bin/console
56
+ - bin/setup
57
+ - ductsclient.gemspec
58
+ - lib/ducts/client.rb
59
+ - lib/ducts/client/version.rb
60
+ - lib/ductsclient.rb
61
+ homepage: https://www.iflab.co.jp/
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ homepage_uri: https://www.iflab.co.jp/
66
+ source_code_uri: https://github.com/iflb/ducts-client-ruby
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.5.0
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.1.6
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: APIs of DUCTS for ruby clients.
86
+ test_files: []