tshield 0.11.14.0 → 0.11.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c729a2d8a181aec850fc2370ddacd7fa2b0eacc901d24bd8d98b8217be4d0213
4
- data.tar.gz: 200d909a38e455cd20349e8dd1b18bfc68b188949ab2daefec52241a7fa6d447
3
+ metadata.gz: d7be1450b970496e9bc17514f42fcda00e98cf8e0b5e269b54b809f38e752653
4
+ data.tar.gz: fd139f9746a3cd6470aec409b809f0d62b6af0915ca0db910a64a7470ad6f44b
5
5
  SHA512:
6
- metadata.gz: e6bed8554facd42b771c91ff8600525d3fc66247476f14e8ae7ef760e60542d7e99b28939c65d087ea27f9fb7a6db9a211234d6faeba7451758b779f624e71e0
7
- data.tar.gz: 9e7273c4cb820af59e2d2f72a417841098d164bd5d4b83cc7e818aa5b4701bd86b334358f6ce11f9515f0f853ee0f407a3d7adc3b6fa9c3bffaa59c24f40db13
6
+ metadata.gz: c9727017f7c245277e64ab4bb4eca0a82f08eaf8ceb0c4b4424693f42dc2267eb0420ea15b557adadba4a69eb3116e7fca60717686a641b8602e77a4d99f183e
7
+ data.tar.gz: c4d633f5d08a2801de7e30e56a2a947b64d3e91db13ca803689a2f05d15053812eeb82585f7058e6a0a2ee9a72170b9d0f5f44557c751f7ce77800073aa23f2e
data/Gemfile CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
- gem 'sinatra-cross_origin'
5
4
  gemspec
data/README.md CHANGED
@@ -13,9 +13,10 @@ TShield is an open source proxy for mocks API responses.
13
13
  * REST
14
14
  * SOAP
15
15
  * Session manager to separate multiple scenarios (success, error, sucess variation, ...)
16
+ * gRPC [EXPERIMENTAL]
16
17
  * Lightweight
17
18
  * MIT license
18
-
19
+
19
20
  ## Table of Contents
20
21
 
21
22
  * [Basic Usage](#basic-usage)
@@ -26,7 +27,7 @@ TShield is an open source proxy for mocks API responses.
26
27
  * [Features](#features)
27
28
  * [Examples](#examples)
28
29
  * [Contributing](#contributing)
29
-
30
+
30
31
  ## Basic Usage
31
32
  ### Install
32
33
 
@@ -37,7 +38,7 @@ TShield is an open source proxy for mocks API responses.
37
38
  To run server execute this command
38
39
 
39
40
  tshield
40
-
41
+
41
42
  Default port is `4567`
42
43
 
43
44
  #### Command Line Options
@@ -102,7 +103,7 @@ To register stub into a session create an object with following attributes:
102
103
  * **session**: name of session.
103
104
  * **stubs**: an array with objects described above.
104
105
 
105
- ### Example of matching configuration
106
+ ### Example of HTTP matching configuration
106
107
 
107
108
  ```json
108
109
  [
@@ -163,7 +164,7 @@ To register stub into a session create an object with following attributes:
163
164
  ]
164
165
  ```
165
166
 
166
- ## Config options for VCR
167
+ ## Config options for HTTP VCR
167
168
  ```yaml
168
169
  request:
169
170
  timeout: 8
@@ -227,7 +228,7 @@ You can use TShield sessions to separate multiple scenarios for your mocks
227
228
  By default TShield save request/response into
228
229
 
229
230
  requests/<<domain_name>>/<<resource_with_param>>/<<http_verb>>/<<index_based.content and json>>
230
-
231
+
231
232
  If you start a session a folder with de **session_name** will be placed between **"requests/"** and **"<<domain_name>>"**
232
233
 
233
234
  ### Start TShield session
@@ -249,6 +250,35 @@ _DELETE_ to http://localhost:4567/sessions
249
250
  curl -X DELETE \
250
251
  http://localhost:4567/sessions
251
252
  ```
253
+ ## [Experimental] Config options for gRPC
254
+
255
+ ```yaml
256
+ grpc:
257
+ port: 5678
258
+ proto_dir: 'proto'
259
+ services:
260
+ 'helloworld_services_pb':
261
+ module: 'Helloworld::Greeter'
262
+ hostname: '0.0.0.0:50051'
263
+ ```
264
+
265
+
266
+ ### Not Implemented Yet
267
+
268
+ - Matching
269
+
270
+ ### Configuration
271
+
272
+ First, generate ruby files from proto files. Use `grpc_tools_ruby_protoc`
273
+ present in the gem `grpc-tools`. Example:
274
+
275
+ `grpc_tools_ruby_protoc -I proto --ruby_out=proto --grpc_out=proto proto/<INPUT>.proto`
276
+
277
+ Call example in component_tests using [grpcurl](https://github.com/fullstorydev/grpcurl):
278
+
279
+ `grpcurl -plaintext -import-path component_tests/proto -proto helloworld.proto -d '{"name": "teste"}' localhost:5678 helloworld.Greeter/SayHello`
280
+
281
+ ### Using in VCR mode
252
282
 
253
283
  ## Custom controllers
254
284
 
data/Rakefile CHANGED
@@ -32,5 +32,5 @@ end
32
32
 
33
33
  task :server do
34
34
  $LOAD_PATH.unshift File.dirname('./lib/tshield.rb')
35
- exec 'bin/tshield'
35
+ Thread.new { exec 'bin/tshield' }
36
36
  end
@@ -5,14 +5,6 @@ require 'tshield/options'
5
5
  TShield::Options.init
6
6
 
7
7
  require 'tshield'
8
- tshield = Thread.new { TShield::Server.run! }
9
8
 
10
- configuration = TShield::Configuration.load_configuration
11
- (configuration.tcp_servers || []).each do |tcp_server|
12
- puts "initializing #{tcp_server['name']}"
13
- require "./servers/#{tcp_server['file']}"
14
- klass = Object.const_get(tcp_server['name'])
15
- Thread.new { klass.new.listen(tcp_server['port']) }
16
- end
17
-
18
- tshield.join
9
+ Thread.new { TShield::Grpc.run! }
10
+ TShield::Server.run!
@@ -1,4 +1,11 @@
1
1
  ---
2
+ grpc:
3
+ port: 5678
4
+ proto_dir: 'proto'
5
+ services:
6
+ 'helloworld_services_pb':
7
+ module: 'Helloworld::Greeter'
8
+ hostname: '0.0.0.0:50051'
2
9
  request:
3
10
  timeout: 10
4
11
  domains:
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'tshield/extensions/string_extensions'
4
4
  require 'tshield/options'
5
- require 'tshield/simple_tcp_server'
6
5
  require 'tshield/server'
6
+ require 'tshield/grpc'
7
7
 
8
8
  # TShield: API mocks for development and testing
9
9
  module TShield
@@ -103,6 +103,11 @@ module TShield
103
103
  session_path || '/sessions'
104
104
  end
105
105
 
106
+ def grpc
107
+ defaults = { 'port' => 5678, 'proto_dir' => 'proto', 'services' => {} }
108
+ defaults.merge(@grpc || {})
109
+ end
110
+
106
111
  def self.get_url_for_domain_by_path(path, config)
107
112
  config['paths'].select { |pattern| path =~ Regexp.new(pattern) }[0]
108
113
  end
@@ -5,6 +5,14 @@ module StringExtensions
5
5
  def to_rack_name
6
6
  "HTTP_#{upcase.tr('-', '_')}"
7
7
  end
8
+
9
+ def underscore
10
+ gsub(/::/, '/')
11
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
12
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
13
+ .tr('-', '_')
14
+ .downcase
15
+ end
8
16
  end
9
17
 
10
18
  String.include StringExtensions
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'grpc'
4
+
5
+ require 'tshield/configuration'
6
+ require 'tshield/grpc/vcr'
7
+
8
+ module TShield
9
+ module Grpc
10
+ module RequestHandler
11
+ include TShield::Grpc::VCR
12
+ def handler(method_name, request, parameters)
13
+ options = self.class.options
14
+ handler_in_vcr_mode(method_name, request, parameters, options)
15
+ end
16
+ end
17
+ def self.run!
18
+ @configuration = TShield::Configuration.singleton.grpc
19
+
20
+ lib_dir = File.join(Dir.pwd, @configuration['proto_dir'])
21
+ $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
22
+
23
+ TShield.logger.info("loading proto files from #{lib_dir}")
24
+
25
+ bind = "0.0.0.0:#{@configuration['port']}"
26
+ TShield.logger.info("Starting gRPC server in #{bind}")
27
+
28
+ server = GRPC::RpcServer.new
29
+ server.add_http2_port(bind, :this_port_is_insecure)
30
+
31
+ services = load_services(@configuration['services'])
32
+ services.each do |class_service|
33
+ class_service.include RequestHandler
34
+ server.handle(class_service)
35
+ end
36
+
37
+ server.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT']) unless services.empty?
38
+ end
39
+
40
+ def self.load_services(services)
41
+ handlers = []
42
+ number_of_handlers = 0
43
+ services.each do |file, options|
44
+ require file
45
+
46
+ base = Object.const_get("#{options['module']}::Service")
47
+ number_of_handlers += 1
48
+
49
+ implementation = build_handler(base, base.rpc_descs, number_of_handlers, options)
50
+ handlers << implementation
51
+ end
52
+ handlers
53
+ end
54
+
55
+ def self.build_handler(base, descriptions, number_of_handlers, options)
56
+ handler = Class.new(base) do
57
+ class << self
58
+ attr_writer :options
59
+ attr_reader :options
60
+ end
61
+ descriptions.each do |service_name, description|
62
+ puts description
63
+ method_name = service_name.to_s.underscore.to_sym
64
+ define_method(method_name) do |request, parameters|
65
+ handler(__method__, request, parameters)
66
+ end
67
+ end
68
+ end
69
+ handler.options = options
70
+ TShield::Grpc.const_set "GrpcService#{number_of_handlers}", handler
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tshield/sessions'
4
+
5
+ module TShield
6
+ module Grpc
7
+ module VCR
8
+ def handler_in_vcr_mode(method_name, request, parameters, options)
9
+ parameters.peer =~ /ipv6:\[(.+?)\]|ipv4:(.+?):/
10
+ peer = Regexp.last_match(1) || Regexp.last_match(2)
11
+
12
+ TShield.logger.info("request from #{parameters.peer}")
13
+ @session = TShield::Sessions.current(peer)
14
+
15
+ TShield.logger.info("grpc using session #{@session || 'default'}")
16
+ module_name = options['module']
17
+
18
+ path = create_destiny(module_name, method_name, request)
19
+ response = saved_response(path)
20
+ if response
21
+ TShield.logger.info("returning saved response for request #{request.to_json} saved into #{hexdigest(request)}")
22
+ return response
23
+ end
24
+
25
+ TShield.logger.info("calling server to get response for #{request.to_json}")
26
+ client_class = Object.const_get("#{module_name}::Stub")
27
+ client_instance = client_class.new(options['hostname'], :this_channel_is_insecure)
28
+ response = client_instance.send(method_name, request)
29
+ save_request_and_response(path, request, response)
30
+ response
31
+ end
32
+
33
+ def saved_response(path)
34
+ response_file = File.join(path, 'response')
35
+ return false unless File.exist? response_file
36
+
37
+ content = JSON.parse File.open(response_file).read
38
+ response_class = File.open(File.join(path, 'response_class')).read.strip
39
+ Kernel.const_get(response_class).new(content)
40
+ end
41
+
42
+ def save_request_and_response(path, request, response)
43
+ save_request(path, request)
44
+ save_response(path, response)
45
+ end
46
+
47
+ def save_request(path, request)
48
+ file = File.open(File.join(path, 'original_request'), 'w')
49
+ file.puts request.to_json
50
+ file.close
51
+ end
52
+
53
+ def save_response(path, response)
54
+ file = File.open(File.join(path, 'response'), 'w')
55
+ file.puts response.to_json
56
+ file.close
57
+
58
+ response_class = File.open(File.join(path, 'response_class'), 'w')
59
+ response_class.puts response.class.to_s
60
+ response_class.close
61
+ end
62
+
63
+ def complete_path(module_name, method_name, request)
64
+ @session_name = (@session || {})[:name]
65
+ path = ['requests', 'grpc', @session_name, module_name, method_name.to_s, hexdigest(request)].compact
66
+ path
67
+ end
68
+
69
+ def create_destiny(module_name, method_name, request)
70
+ current_path = []
71
+
72
+ path = complete_path(module_name, method_name, request)
73
+ TShield.logger.info("using path #{path}")
74
+ path.each do |path|
75
+ current_path << path
76
+ destiny = File.join current_path
77
+ Dir.mkdir destiny unless File.exist? destiny
78
+ end
79
+ path
80
+ end
81
+
82
+ def hexdigest(request)
83
+ Digest::SHA1.hexdigest request.to_json
84
+ end
85
+ end
86
+ end
87
+ end
@@ -57,7 +57,6 @@ module TShield
57
57
 
58
58
  def self.run!
59
59
  register_resources
60
- require 'byebug'
61
60
  super
62
61
  end
63
62
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'byebug'
4
3
  require 'tshield/counter'
5
4
 
6
5
  module TShield
@@ -9,14 +8,17 @@ module TShield
9
8
  # Start and stop session for ip
10
9
  module Sessions
11
10
  def self.start(ip, name)
11
+ TShield.logger.info("starting session #{name} for ip #{normalize_ip(ip)}")
12
12
  sessions[normalize_ip(ip)] = { name: name, counter: TShield::Counter.new }
13
13
  end
14
14
 
15
15
  def self.stop(ip)
16
+ TShield.logger.info("stoping session for ip #{normalize_ip(ip)}")
16
17
  sessions[normalize_ip(ip)] = nil
17
18
  end
18
19
 
19
20
  def self.current(ip)
21
+ TShield.logger.info("fetching session for ip #{normalize_ip(ip)}")
20
22
  sessions[normalize_ip(ip)]
21
23
  end
22
24
 
@@ -5,7 +5,7 @@ module TShield
5
5
  class Version
6
6
  MAJOR = 0
7
7
  MINOR = 11
8
- PATCH = 14
8
+ PATCH = 19
9
9
  PRE = 0
10
10
 
11
11
  class << self
@@ -15,9 +15,4 @@ require 'webmock/rspec'
15
15
  require 'tshield/extensions/string_extensions'
16
16
 
17
17
  RSpec.configure do |config|
18
- config.before(:each) do
19
- allow(File).to receive(:join).and_return(
20
- 'spec/tshield/fixtures/config/tshield.yml'
21
- )
22
- end
23
18
  end
@@ -35,6 +35,12 @@ describe TShield::Configuration do
35
35
  )
36
36
  end
37
37
 
38
+ context 'on grpc configuration' do
39
+ it 'recover server port' do
40
+ expect(@configuration.grpc['port']).to(eq(5678))
41
+ end
42
+ end
43
+
38
44
  context 'on load filters' do
39
45
  it 'recover filters for a domain' do
40
46
  expect(@configuration.get_filters('example.org')).to eq([ExampleFilter])
@@ -72,4 +78,17 @@ describe TShield::Configuration do
72
78
  expect { TShield::Configuration.singleton }.to raise_error RuntimeError
73
79
  end
74
80
  end
81
+
82
+ context 'on config exists without grpc entry' do
83
+ before :each do
84
+ options_instance = double
85
+ allow(options_instance).to receive(:configuration_file)
86
+ .and_return('spec/tshield/fixtures/config/tshield-without-grpc.yml')
87
+ allow(TShield::Options).to receive(:instance).and_return(options_instance)
88
+ @configuration = TShield::Configuration.singleton
89
+ end
90
+ it 'should set default value for port' do
91
+ expect(@configuration.grpc).to eql('port' => 5678, 'proto_dir' => 'proto', 'services' => {})
92
+ end
93
+ end
75
94
  end
@@ -0,0 +1,17 @@
1
+ ---
2
+ request:
3
+ timeout: 0
4
+ domains:
5
+ 'example.org':
6
+ name: 'example.org'
7
+ filters:
8
+ - 'ExampleFilter'
9
+ paths:
10
+ - '/api/one'
11
+ - '/api/two'
12
+ skip_query_params:
13
+ - 'a'
14
+ 'example.com':
15
+ name: 'example.com'
16
+ paths:
17
+ - '/api/three'
@@ -1,4 +1,6 @@
1
1
  ---
2
+ grpc:
3
+ port: 5678
2
4
  request:
3
5
  timeout: 0
4
6
  domains:
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestServices
4
+ class Service
5
+ def self.rpc_descs
6
+ { 'ServiceMethod' => {} }
7
+ end
8
+ end
9
+
10
+ class Stub
11
+ def initialize(attributes, options = {}); end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'tshield/grpc'
6
+
7
+ describe TShield::Grpc do
8
+ context 'on load services' do
9
+ before :each do
10
+ lib_dir = File.join(__dir__, 'fixtures/proto')
11
+ $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ @services = {
14
+ 'test_services_pb' => { 'module' => 'TestServices', 'hostname' => '0.0.0.0:5678' }
15
+ }
16
+ end
17
+
18
+ it 'should implement a service from options' do
19
+ implementation = TShield::Grpc.load_services(@services).first
20
+ instance = implementation.new
21
+ expect(instance.respond_to?(:service_method)).to be_truthy
22
+ end
23
+ end
24
+ end
@@ -25,6 +25,8 @@ Gem::Specification.new do |s|
25
25
  s.required_ruby_version = '>= 2.3'
26
26
 
27
27
  s.add_dependency('byebug', '~> 11.0', '>= 11.0.1')
28
+ s.add_dependency('grpc', '~> 1.28', '>= 1.28.0')
29
+ s.add_dependency('grpc-tools', '~> 1.28', '>= 1.28.0')
28
30
  s.add_dependency('httparty', '~> 0.14', '>= 0.14.0')
29
31
  s.add_dependency('json', '~> 2.0', '>= 2.0')
30
32
  s.add_dependency('puma', '~> 4.3', '>= 4.3.3')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tshield
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.14.0
4
+ version: 0.11.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Diego Rubin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-04-23 00:00:00.000000000 Z
12
+ date: 2020-07-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: byebug
@@ -31,6 +31,46 @@ dependencies:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: 11.0.1
34
+ - !ruby/object:Gem::Dependency
35
+ name: grpc
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.28.0
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '1.28'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 1.28.0
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.28'
54
+ - !ruby/object:Gem::Dependency
55
+ name: grpc-tools
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.28.0
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '1.28'
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 1.28.0
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.28'
34
74
  - !ruby/object:Gem::Dependency
35
75
  name: httparty
36
76
  requirement: !ruby/object:Gem::Requirement
@@ -387,6 +427,8 @@ files:
387
427
  - lib/tshield/controllers/sessions.rb
388
428
  - lib/tshield/counter.rb
389
429
  - lib/tshield/extensions/string_extensions.rb
430
+ - lib/tshield/grpc.rb
431
+ - lib/tshield/grpc/vcr.rb
390
432
  - lib/tshield/logger.rb
391
433
  - lib/tshield/matching/filters.rb
392
434
  - lib/tshield/options.rb
@@ -396,15 +438,17 @@ files:
396
438
  - lib/tshield/response.rb
397
439
  - lib/tshield/server.rb
398
440
  - lib/tshield/sessions.rb
399
- - lib/tshield/simple_tcp_server.rb
400
441
  - lib/tshield/version.rb
401
442
  - spec/spec_helper.rb
402
443
  - spec/tshield/after_filter_spec.rb
403
444
  - spec/tshield/configuration_spec.rb
404
445
  - spec/tshield/controllers/requests_spec.rb
446
+ - spec/tshield/fixtures/config/tshield-without-grpc.yml
405
447
  - spec/tshield/fixtures/config/tshield.yml
406
448
  - spec/tshield/fixtures/filters/example_filter.rb
407
449
  - spec/tshield/fixtures/matching/example.json
450
+ - spec/tshield/fixtures/proto/test_services_pb.rb
451
+ - spec/tshield/grpc_spec.rb
408
452
  - spec/tshield/options_spec.rb
409
453
  - spec/tshield/request_matching_spec.rb
410
454
  - spec/tshield/request_vcr_spec.rb
@@ -435,11 +479,14 @@ summary: Proxy for mocks API responses
435
479
  test_files:
436
480
  - spec/spec_helper.rb
437
481
  - spec/tshield/request_matching_spec.rb
482
+ - spec/tshield/grpc_spec.rb
438
483
  - spec/tshield/configuration_spec.rb
439
484
  - spec/tshield/request_vcr_spec.rb
440
485
  - spec/tshield/controllers/requests_spec.rb
441
486
  - spec/tshield/options_spec.rb
442
487
  - spec/tshield/after_filter_spec.rb
443
488
  - spec/tshield/fixtures/matching/example.json
489
+ - spec/tshield/fixtures/proto/test_services_pb.rb
444
490
  - spec/tshield/fixtures/filters/example_filter.rb
445
491
  - spec/tshield/fixtures/config/tshield.yml
492
+ - spec/tshield/fixtures/config/tshield-without-grpc.yml
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'socket'
4
-
5
- module TShield
6
- class SimpleTCPServer
7
- def initialize
8
- @running = true
9
- end
10
-
11
- def on_connect(_client)
12
- raise 'should implement method on_connect'
13
- end
14
-
15
- def close
16
- @running = false
17
- end
18
-
19
- def listen(port)
20
- puts "listening #{port}"
21
- @server = TCPServer.new(port)
22
- while @running
23
- client = @server.accept
24
- on_connect(client)
25
- end
26
- end
27
- end
28
- end