canson 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.env +0 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +15 -0
- data/Gemfile +18 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/iodine +49 -0
- data/bin/setup +8 -0
- data/canson.gemspec +45 -0
- data/lib/canson.rb +16 -0
- data/lib/canson/base.rb +129 -0
- data/lib/canson/responder.rb +18 -0
- data/lib/canson/route_map.rb +19 -0
- data/lib/canson/version.rb +5 -0
- data/lib/canson/web_socket.rb +44 -0
- data/refs.md +113 -0
- metadata +235 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 19cfa746b443d79afafcaf8feeb8336e8f42ef80
|
4
|
+
data.tar.gz: 2169d634254c158235eea0fc4c49078fccb5934f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6423e71a01382ca72b94a1d5c90006add6197ca3b5b083dda1f187ae37e652d1312acb5e4a45c2bf52c948405834926fbdb3d7fd751bc7436a316855c6b893a7
|
7
|
+
data.tar.gz: cc584f2eab5c44b0a47773129821c9e82191007552a3038e5d7caa449a1e5673f159030d3669d7705394799e8e4de64871b6eba3d9bc45166e1f06c81a7d8702
|
data/.env
ADDED
File without changes
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
Exclude:
|
5
|
+
- 'bin/*'
|
6
|
+
- 'config/**/*'
|
7
|
+
- 'deps/**/*'
|
8
|
+
- 'spec/**/*'
|
9
|
+
- 'test/**/*'
|
10
|
+
- 'lib/erlport/**/*'
|
11
|
+
- 'Guardfile'
|
12
|
+
- 'Rakefile'
|
13
|
+
- 'Gemfile'
|
14
|
+
- 'canson.gemspec'
|
15
|
+
- '*.ex'
|
16
|
+
- '*.exs'
|
17
|
+
|
18
|
+
Metrics/CyclomaticComplexity:
|
19
|
+
Max: 9
|
20
|
+
|
21
|
+
Metrics/PerceivedComplexity:
|
22
|
+
Max: 9
|
23
|
+
|
24
|
+
Metrics/ClassLength:
|
25
|
+
Max: 250
|
26
|
+
|
27
|
+
# If you are using the --enable-frozen-string-literal flag
|
28
|
+
# You might also want to add this:
|
29
|
+
Style/FrozenStringLiteralComment:
|
30
|
+
Enabled: true
|
31
|
+
|
32
|
+
Style/SafeNavigation:
|
33
|
+
# Safe navigation may cause a statement to start returning `nil` in addition
|
34
|
+
# to whatever it used to return.
|
35
|
+
ConvertCodeThatCanStartToReturnNil: true
|
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
cache: bundler
|
4
|
+
rvm:
|
5
|
+
- 2.3.1
|
6
|
+
bundler_args: --jobs=2
|
7
|
+
cache: bundler
|
8
|
+
before_install: gem install bundler -v 1.13.6
|
9
|
+
|
10
|
+
script:
|
11
|
+
- bundle install
|
12
|
+
- rake
|
13
|
+
- gem install bundler-audit
|
14
|
+
- bundle-audit update
|
15
|
+
- bundle-audit check
|
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
group :development, :test do
|
5
|
+
gem 'bundler', '~> 1.7'
|
6
|
+
gem 'rake', '~> 11.3.0'
|
7
|
+
gem 'byebug'
|
8
|
+
gem 'minitest', '~> 5.0'
|
9
|
+
gem 'guard', '~> 2.14.0'
|
10
|
+
gem 'guard-bundler', '~> 2.1.0'
|
11
|
+
gem 'guard-minitest', '~> 2.4.6'
|
12
|
+
gem 'rubocop', '~> 0.45.0'
|
13
|
+
gem 'guard-rubocop', '~> 1.2.0'
|
14
|
+
gem 'minitest-reporters', '~> 1.1.11'
|
15
|
+
gem 'minitest-vcr', '~> 1.4.0'
|
16
|
+
gem 'webmock', '~> 2.1.0'
|
17
|
+
gem "rack-test", "~> 0.6"
|
18
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# require 'rubocop'
|
2
|
+
# require 'dotenv'
|
3
|
+
# Dotenv.load(File.expand_path('../.env.test', __FILE__))
|
4
|
+
|
5
|
+
guard :bundler do
|
6
|
+
require 'guard/bundler'
|
7
|
+
require 'guard/bundler/verify'
|
8
|
+
helper = Guard::Bundler::Verify.new
|
9
|
+
files = ['Gemfile']
|
10
|
+
files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
|
11
|
+
# Assume files are symlinked from somewhere
|
12
|
+
files.each { |file| watch(helper.real_path(file)) }
|
13
|
+
end
|
14
|
+
|
15
|
+
guard :minitest, test_folders: 'spec', test_file_patterns: '*_spec.rb', all_on_start: false do
|
16
|
+
watch(%r{^spec/(.*)_spec\.rb$})
|
17
|
+
watch(%r{^lib/canson/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
18
|
+
watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
|
19
|
+
end
|
20
|
+
|
21
|
+
guard :rubocop, all_on_start: false do
|
22
|
+
watch(%r{.+\.rb$})
|
23
|
+
watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
|
24
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Jaigouk Kim
|
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,132 @@
|
|
1
|
+
# Canson
|
2
|
+
|
3
|
+
small rack based framework that can run websocket. 20K connections can be handled
|
4
|
+
|
5
|
+
## Run the example chat app
|
6
|
+
|
7
|
+
```
|
8
|
+
cd spec/test_app_root
|
9
|
+
bundle install
|
10
|
+
bundler exec iodine -p 3000 -t 16 -w 4
|
11
|
+
```
|
12
|
+
|
13
|
+
open localhost:3000 in browser
|
14
|
+
|
15
|
+
## Example app
|
16
|
+
|
17
|
+
in `spec/test_app_root`
|
18
|
+
|
19
|
+
```
|
20
|
+
require 'canson'
|
21
|
+
class TestApp < Canson::Base
|
22
|
+
|
23
|
+
def self.print_out
|
24
|
+
puts 'hijack'
|
25
|
+
end
|
26
|
+
|
27
|
+
get '/' do
|
28
|
+
print_out
|
29
|
+
{results: 'hi'}
|
30
|
+
end
|
31
|
+
|
32
|
+
get '/ask' do |params|
|
33
|
+
name = params[:name]
|
34
|
+
{results: name}
|
35
|
+
end
|
36
|
+
|
37
|
+
on_open do
|
38
|
+
puts '================================'
|
39
|
+
puts 'We have a websocket connection'
|
40
|
+
puts '================================'
|
41
|
+
end
|
42
|
+
|
43
|
+
on_close do
|
44
|
+
puts "Bye Bye... #{count} connections left..."
|
45
|
+
end
|
46
|
+
|
47
|
+
on_shutdown do
|
48
|
+
write 'The server is shutting down, goodbye.'
|
49
|
+
end
|
50
|
+
|
51
|
+
on_message do |params|
|
52
|
+
data = params[:data]
|
53
|
+
ws = params[:ws]
|
54
|
+
nickname = params[:nickname]
|
55
|
+
tmp = "#{nickname}: #{data}"
|
56
|
+
ws.write tmp
|
57
|
+
ws.each { |h| h.write tmp }
|
58
|
+
puts '================================'
|
59
|
+
puts "got message: #{data} encoded as #{data.encoding}"
|
60
|
+
puts "broadcasting #{tmp.bytesize} bytes with encoding #{tmp.encoding}"
|
61
|
+
puts '================================'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
```
|
67
|
+
require './test_app.rb'
|
68
|
+
run TestApp.new
|
69
|
+
```
|
70
|
+
|
71
|
+
|
72
|
+
## Usage
|
73
|
+
|
74
|
+
Given the following piece of ruby code:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
# config.ru
|
78
|
+
|
79
|
+
require "canson"
|
80
|
+
|
81
|
+
get "/index" do
|
82
|
+
{ results: [1, 2, 3] }
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
> The server is run via `bundle exec rackup --port 3000`.
|
87
|
+
|
88
|
+
When requested with `curl http://localhost:3000/bla -i`, it should return:
|
89
|
+
|
90
|
+
```
|
91
|
+
HTTP/1.1 200 OK
|
92
|
+
Content-Type: application/json
|
93
|
+
|
94
|
+
{"results": [1, 2, 3]}
|
95
|
+
```
|
96
|
+
|
97
|
+
Given the following piece of ruby code:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# config.ru
|
101
|
+
|
102
|
+
require "trialday"
|
103
|
+
|
104
|
+
get "/bla" do
|
105
|
+
{ results: [1, 2, 3] }
|
106
|
+
end
|
107
|
+
|
108
|
+
post "/bla" do |params|
|
109
|
+
name = params[:name]
|
110
|
+
|
111
|
+
{ name: name }
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
When requested with `curl http://localhost:3000/index -i`, it should return:
|
116
|
+
|
117
|
+
```
|
118
|
+
HTTP/1.1 200 OK
|
119
|
+
Content-Type: application/json
|
120
|
+
|
121
|
+
{"results": [1, 2, 3]}
|
122
|
+
```
|
123
|
+
|
124
|
+
When requested with `curl -XPOST http://localhost:3000/bla -i -H "Content-Type: application/json" -d '{"name": "Mario"}'`, it should return:
|
125
|
+
|
126
|
+
```
|
127
|
+
HTTP/1.1 200 OK
|
128
|
+
Content-Type: application/json
|
129
|
+
|
130
|
+
{"name": "Mario"}
|
131
|
+
```
|
132
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'dotenv'
|
4
|
+
require 'dotenv/tasks'
|
5
|
+
Dotenv.load(File.expand_path('../.env.test', __FILE__))
|
6
|
+
|
7
|
+
Rake::TestTask.new(test: :dotenv) do |t|
|
8
|
+
t.libs << 'spec'
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "canson"
|
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
|
data/bin/iodine
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'iodine'
|
4
|
+
require 'rack'
|
5
|
+
|
6
|
+
if ARGV[0] =~ /(\-\?)|(help)|(\?)$/
|
7
|
+
puts <<-EOS
|
8
|
+
Iodine's HTTP/Websocket server version #{Iodine::VERSION}
|
9
|
+
Use:
|
10
|
+
iodine <options> <filename>
|
11
|
+
Both <options> and <filename> are optional.
|
12
|
+
Available options:
|
13
|
+
-p Port number. Default: 3000.
|
14
|
+
-t Number of threads. Default: 1 => single worker thread.
|
15
|
+
-w Number of worker processes. Default: 1 => a single process.
|
16
|
+
-www Public folder for static file serving. Default: nil (none).
|
17
|
+
-v Log responses.
|
18
|
+
-q Never log responses.
|
19
|
+
-warmup Warmup invokes autoloading (lazy loading) during server startup.
|
20
|
+
-maxbd Maximum Mb per HTTP message (max body size). Default: ~50Mb.
|
21
|
+
-maxms Maximum Bytes per Websocket message. Default: ~250Kb.
|
22
|
+
-ping Websocket ping interval in seconds. Default: 40 seconds.
|
23
|
+
<filename> Defaults to: config.ru
|
24
|
+
Example:
|
25
|
+
iodine -p 80
|
26
|
+
iodine -p 8080 path/to/app/conf.ru
|
27
|
+
iodine -p 8080 -w 4 -t 16
|
28
|
+
EOS
|
29
|
+
exit(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
filename = (ARGV[-2].to_s[0] != '-' && ARGV[-1].to_s[0] != '-' && ARGV[-1]) || 'config.ru'
|
33
|
+
app, opt = Rack::Builder.parse_file filename
|
34
|
+
if ARGV.index('-maxbd') && ARGV[ARGV.index('-maxbd') + 1]
|
35
|
+
Iodine::Rack.max_body_size = ARGV[ARGV.index('-maxbd') + 1].to_i
|
36
|
+
end
|
37
|
+
if ARGV.index('-maxms') && ARGV[ARGV.index('-maxms') + 1]
|
38
|
+
Iodine::Rack.max_msg_size = ARGV[ARGV.index('-maxms') + 1].to_i
|
39
|
+
end
|
40
|
+
if ARGV.index('-ping') && ARGV[ARGV.index('-ping') + 1]
|
41
|
+
Iodine::Rack.ws_timeout = ARGV[ARGV.index('-ping') + 1].to_i
|
42
|
+
end
|
43
|
+
if ARGV.index('-www') && ARGV[ARGV.index('-www') + 1]
|
44
|
+
Iodine::Rack.public = ARGV[ARGV.index('-www') + 1]
|
45
|
+
end
|
46
|
+
Iodine::Rack.log = true if ARGV.index('-v')
|
47
|
+
Iodine::Rack.log = false if ARGV.index('-q')
|
48
|
+
Iodine.warmup if ARGV.index('-warmup')
|
49
|
+
Iodine::Rack.run(app, opt)
|
data/bin/setup
ADDED
data/canson.gemspec
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'canson/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "canson"
|
8
|
+
spec.version = Canson::VERSION
|
9
|
+
spec.authors = ["Jaigouk Kim"]
|
10
|
+
spec.email = ["ping@jaigouk.kim"]
|
11
|
+
|
12
|
+
spec.summary = %q{grpc}
|
13
|
+
spec.description = %q{grpc based webframework}
|
14
|
+
spec.homepage = "https://github.com/jaigouk/canson"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
# if spec.respond_to?(:metadata)
|
20
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
21
|
+
# else
|
22
|
+
# raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
# "public gem pushes."
|
24
|
+
# end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "bin"
|
30
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_dependency 'dry-monads', '~> 0.2.1'
|
34
|
+
spec.add_dependency 'dry-matcher', '~> 0.5'
|
35
|
+
spec.add_dependency 'dry-transaction', '~> 0.8'
|
36
|
+
spec.add_dependency 'grpc', '~> 1.0.1'
|
37
|
+
spec.add_dependency 'http-2', '~> 0.8.2'
|
38
|
+
spec.add_dependency 'http_parser.rb', '~> 0.6'
|
39
|
+
spec.add_dependency 'dotenv'
|
40
|
+
spec.add_runtime_dependency 'iodine', '~> 0.2.3'
|
41
|
+
spec.add_runtime_dependency 'rack', '~> 2.0.1'
|
42
|
+
spec.add_runtime_dependency 'tilt', '~> 2.0.5'
|
43
|
+
spec.add_runtime_dependency 'mustermann', '~> 0.4'
|
44
|
+
spec.add_runtime_dependency 'mime-types', '~> 2.4'
|
45
|
+
end
|
data/lib/canson.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tilt'
|
4
|
+
require 'rack'
|
5
|
+
require 'mustermann'
|
6
|
+
require 'mime-types'
|
7
|
+
require 'canson/version'
|
8
|
+
require 'canson/web_socket'
|
9
|
+
require 'canson/route_map'
|
10
|
+
require 'canson/responder'
|
11
|
+
require 'canson/base'
|
12
|
+
|
13
|
+
module Canson
|
14
|
+
class RequestError
|
15
|
+
end
|
16
|
+
end
|
data/lib/canson/base.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/utils'
|
4
|
+
require 'rack/media_type'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Canson
|
8
|
+
# used to create a new app
|
9
|
+
# App < CansonBase
|
10
|
+
# get '/foo' do
|
11
|
+
# {result: 'ok'}
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
class Base
|
15
|
+
extend Forwardable
|
16
|
+
include Mustermann
|
17
|
+
|
18
|
+
attr_reader :klass
|
19
|
+
attr_accessor :responder
|
20
|
+
|
21
|
+
def_delegators :@klass, :routes, :method_missing, :filename
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# def inherited(subclass)
|
25
|
+
# # subclass.routes @routes
|
26
|
+
# subclass.filename @filename
|
27
|
+
# end
|
28
|
+
|
29
|
+
[:on_open, :on_close, :on_shutdown, :on_message].each do |m|
|
30
|
+
define_method m do |&block|
|
31
|
+
@responder = Responder.new :ws, &block
|
32
|
+
routes[m]['websocket_method'] = @responder
|
33
|
+
@responder.call
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
[:get, :post, :put, :delete, :options].each do |m|
|
38
|
+
define_method m do |path, opts = {}, &block|
|
39
|
+
path = ::Mustermann.new path, opts
|
40
|
+
@responder = Responder.new m, &block
|
41
|
+
routes[m][path.to_s] = @responder
|
42
|
+
@responder.call
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def routes(r = nil)
|
47
|
+
return @routes = r if r
|
48
|
+
@routes ||= Hash.new { |h, k| h[k] = RouteMap.new }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(responder = Responder.new)
|
53
|
+
@responder = responder
|
54
|
+
@klass = self.class
|
55
|
+
@websocket = nil
|
56
|
+
@filename = File.expand_path('./index.html')
|
57
|
+
end
|
58
|
+
|
59
|
+
def filename
|
60
|
+
@filename
|
61
|
+
end
|
62
|
+
|
63
|
+
def call(env)
|
64
|
+
m = env['REQUEST_METHOD'].tr('/', '').downcase.to_sym
|
65
|
+
target = routes[m][env['PATH_INFO']]
|
66
|
+
|
67
|
+
if m == :get && env['PATH_INFO'] == '/' && File.file?(filename)
|
68
|
+
out = File.open(filename)
|
69
|
+
return [200, { 'X-Sendfile' => filename,
|
70
|
+
'Content-Length' => out.size }, out]
|
71
|
+
end
|
72
|
+
|
73
|
+
return handle_socket(env) if env['HTTP_UPGRADE'] == 'websocket'
|
74
|
+
return [404, {}, ['no route']] unless target
|
75
|
+
return call_result(env, target)
|
76
|
+
rescue => e
|
77
|
+
puts e.message
|
78
|
+
[500, {}, ["server error #{e.message}".to_json]]
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_socket(env)
|
82
|
+
nickname = env['PATH_INFO'][1..-1].force_encoding 'UTF-8'
|
83
|
+
if env['HTTP_UPGRADE'.freeze] =~ /websocket/i
|
84
|
+
routes[:on_message][nickname] ||= Canson::Websocket.new(env)
|
85
|
+
routes[:on_message][nickname].on_open = routes[:on_open]['websocket_method']
|
86
|
+
routes[:on_message][nickname].on_close = routes[:on_close]['websocket_method']
|
87
|
+
routes[:on_message][nickname].on_shutdown = routes[:on_shutdown]['websocket_method']
|
88
|
+
routes[:on_message][nickname].on_message = routes[:on_message]['websocket_method']
|
89
|
+
env['upgrade.websocket'.freeze] = routes[:on_message][nickname]
|
90
|
+
return [0, {}, []]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def call_result(env, target)
|
97
|
+
param = get_param(env)
|
98
|
+
result = if param.nil?
|
99
|
+
self.class.instance_exec(&(target.call))
|
100
|
+
else
|
101
|
+
self.class.instance_exec(param, &(target.call))
|
102
|
+
end
|
103
|
+
return [404, {}, ['not found']] if result.values.include? nil
|
104
|
+
[200, { 'Content-Type' => 'application/json' }, [result.to_json]]
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_param(env)
|
108
|
+
req = Rack::Request.new(env)
|
109
|
+
param = req.params.empty? ? req.body.read : req.params
|
110
|
+
return if param.empty?
|
111
|
+
param_to_hash(param)
|
112
|
+
end
|
113
|
+
|
114
|
+
def param_to_hash(param)
|
115
|
+
if param.class == Hash
|
116
|
+
if param.keys.first.include?(':')
|
117
|
+
return to_hash(JSON.parse(param.keys.first))
|
118
|
+
end
|
119
|
+
to_hash(param)
|
120
|
+
else
|
121
|
+
to_hash(JSON.parse(param))
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_hash(ele)
|
126
|
+
ele.map { |k, v| [k.to_sym, v] }.to_h
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Canson
|
4
|
+
# responsible to return the block
|
5
|
+
class Responder
|
6
|
+
attr_reader :method, :response_handler
|
7
|
+
attr_writer :base
|
8
|
+
|
9
|
+
def initialize(method = nil, &block)
|
10
|
+
@method = method
|
11
|
+
@response_handler = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
@response_handler
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Canson
|
4
|
+
# keeps routes and responders
|
5
|
+
class RouteMap
|
6
|
+
def initialize
|
7
|
+
@hash = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(route, responder)
|
11
|
+
@hash[route] = responder
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](route)
|
15
|
+
_path, responder = @hash.find { |k, _v| k.match(route) }
|
16
|
+
responder
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Canson
|
2
|
+
class Websocket
|
3
|
+
def initialize(env)
|
4
|
+
@nickname = env['PATH_INFO'][1..-1].force_encoding 'UTF-8'
|
5
|
+
@on_open = nil
|
6
|
+
@on_close = nil
|
7
|
+
@on_shutdown = nil
|
8
|
+
@on_message = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_open=(responder)
|
12
|
+
@on_open = responder
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_close=(responder)
|
16
|
+
@on_close = responder
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_shutdown=(responder)
|
20
|
+
@on_shutdown = responder
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_message=(responder)
|
24
|
+
@on_message = responder
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_open
|
28
|
+
@on_open.class.instance_exec({count: self.count}, &(@on_open.call))
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_close
|
32
|
+
@on_close.class.instance_exec({count: self.count}, &(@on_close.call))
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_shutdown
|
36
|
+
@on_shutdown.class.instance_exec(&(@on_shutdown.call))
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_message(data)
|
40
|
+
@on_message.class.instance_exec({data: data, ws: self, nickname: @nickname},
|
41
|
+
&(@on_message.call))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/refs.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
|
2
|
+
## Parsing
|
3
|
+
|
4
|
+
https://github.com/sinatra/mustermann
|
5
|
+
https://github.com/tmm1/http_parser.rb
|
6
|
+
https://github.com/ngauthier/tubesock
|
7
|
+
|
8
|
+
## Streaming
|
9
|
+
|
10
|
+
https://rosenfeld.herokuapp.com/en/articles/ruby-rails/2016-07-02-the-sad-state-of-streaming-in-ruby-web-applications
|
11
|
+
Each request thread (or process) has access to a "response" object that accepts a "write" call that goes directly to the socket's output (or after a "flush" call).
|
12
|
+
It would be awesome if Ruby web applications had the option to use a more flexible API, more friendly to streamed responses, including SSE and websockets. Hijacking currently seems to be considered a second-class citizen since they are usually ignored by major web frameworks like Rails itself.
|
13
|
+
|
14
|
+
https://bowild.wordpress.com/2016/07/31/the-dark-side-of-the-rack/
|
15
|
+
what if everything we had to do to upgrade from HTTP to Websockets was something like this env[upgrade.websockets] = MyCallbacks.new(env)…?
|
16
|
+
|
17
|
+
We can unify the IO polling for all IO objects (HTTP, Websocket, no need for hijack). This is bigger then you think.
|
18
|
+
We can parse websocket data before entering the GIL, winning some of our concurrency back. This also means we can better utilize multi-core CPUs.
|
19
|
+
|
20
|
+
We don’t need to know anything about network programming – let the people programming servers do what they do best and let the people who write web applications focus on their application logic.
|
21
|
+
https://gist.github.com/boazsegev/1466442c913a8dd4271178cab9d98a27
|
22
|
+
https://github.com/boazsegev/iodine
|
23
|
+
|
24
|
+
http://rubykaigi.org/2015/presentations/tenderlove
|
25
|
+
|
26
|
+
https://github.com/SamSaffron/message_bus
|
27
|
+
|
28
|
+
rack hijack
|
29
|
+
https://github.com/ngauthier/tubesock
|
30
|
+
|
31
|
+
https://bowild.wordpress.com/2016/07/31/the-dark-side-of-the-rack/
|
32
|
+
|
33
|
+
https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable
|
34
|
+
|
35
|
+
|
36
|
+
https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable
|
37
|
+
|
38
|
+
|
39
|
+
https://klibert.pl/statics/python-and-elixir/#/
|
40
|
+
|
41
|
+
https://www.reddit.com/r/ruby/comments/4r9alo/the_sad_state_of_streaming_support_in_ruby_web/
|
42
|
+
|
43
|
+
If your servers were implementing HTTP/2, then hijacking the IO would break the protocol and (hopefully) terminate the connection with no data being received.
|
44
|
+
HTTP/2 is a binary protocol with packet headers and footer. If the hijacked IO doesn't write the data as HTTP/2 packets and sends the end of stream flag upon completion, then the receiving party will simply assume the data is garbage and (if implementing the standard correctly) will disconnect.
|
45
|
+
This is the reason I though hijacking is limited and approaching an end-of-life cycle for HTTP requests (will soon be impossible). It's only possible long term implementation is HTTP upgrade requests which usually means a long-lived connection is being established with either Websockets or HTTP/2 (although other protocols can also be used with the technique).
|
46
|
+
|
47
|
+
https://gist.github.com/TiagoCardoso1983/591d48cfde04219f801fa7fd7966571c
|
48
|
+
|
49
|
+
For http2, the same above stands, with a few add-ons:
|
50
|
+
Ruby's openssl is (at least, it was) missing some bugfixes for ALPN negotiation (is it still valid?).
|
51
|
+
|
52
|
+
Ruby is currently missing an optimized parser for http2 requests (not a big hinderance, as there's an http2 pure ruby gem around).
|
53
|
+
http2-to-http1 on the web server is seemingly "good enough".
|
54
|
+
http2 protocol is only a big gain if one is using an evented application server, as the big thing over there is the frame multiplexing.
|
55
|
+
|
56
|
+
So, we have thin and reel. Both of them use C-written event loops (reel uses libev), which means we're out of ruby core, and are not widely used (correct me if I'm wrong, but is reel compatible with most available middlewares? Is it running rails for anyone?).
|
57
|
+
|
58
|
+
rack needs to die and be born again. event-based middlewares, #call(request, response) instead of #call(env), a proper stream object with less socket-y semantics, the rack hijack hack won't stand.
|
59
|
+
|
60
|
+
nio4r (celluloid-io/reel) looks much better, but ships with a patched version of libev because no-ruby-core-GIL-API). It brings with it the libev's "problems", which are no Windows support or no support for not-network file descriptors (file, pipes) (not really a problem when there is no competition though).
|
61
|
+
|
62
|
+
### grpc
|
63
|
+
|
64
|
+
* stackoverflow
|
65
|
+
|
66
|
+
http://stackoverflow.com/questions/35065875/how-to-bring-a-grpc-defined-api-to-the-web-browser
|
67
|
+
|
68
|
+
We want to build a Javascript/HTML gui for our gRPC-microservices. Since gRPC is not supported on the browser side, we thought of using web-sockets to connect to a node.js server, which calls the target service via grpc. We struggle to find an elegant solution to do this. Especially, since we use gRPC streams to push events between our micro-services. It seems that we need a second RPC system, just to communicate between the front end and the node.js server. This seems to be a lot of overhead and additional code that must be maintained.
|
69
|
+
|
70
|
+
Does anyone have experience doing something like this or has an idea how this could be solved?
|
71
|
+
|
72
|
+
https://github.com/tmc/grpc-websocket-proxy sounds like it may meet your needs. This translates json over web sockets to grpc (layer on top of grpc-gateway).
|
73
|
+
|
74
|
+
|
75
|
+
https://coreos.com/blog/gRPC-protobufs-swagger.html
|
76
|
+
|
77
|
+
One of the key reasons we chose gRPC is because it uses HTTP/2, enabling applications to present both a HTTP 1.1 REST+JSON API and an efficient gRPC interface on a single TCP port. This gives developers compatibility with the REST web ecosystem while advancing a new, high-efficiency RPC protocol. With Go 1.6 recently released, Go ships with a stable net/http2 package by default.
|
78
|
+
|
79
|
+
|
80
|
+
https://github.com/grpc-ecosystem/grpc-gateway
|
81
|
+
|
82
|
+
https://github.com/ruby-concurrency/concurrent-ruby/blob/master/doc/channel.md
|
83
|
+
|
84
|
+
https://github.com/paralin/grpc-bus
|
85
|
+
|
86
|
+
# RubyConf 2016
|
87
|
+
|
88
|
+
RubyConf 2016 - To Clojure and back: writing and rewriting in Ruby by Phill MV
|
89
|
+
https://www.youtube.com/watch?v=doZ0XAc9Wtc
|
90
|
+
|
91
|
+
1. "value" objects
|
92
|
+
tcrayford/values
|
93
|
+
|
94
|
+
2. "manager" object
|
95
|
+
controller: handles input
|
96
|
+
model: value object. handles persistence(db query, save)
|
97
|
+
manager: handles state
|
98
|
+
```
|
99
|
+
class PackageManager
|
100
|
+
attr_reader :platform, :release
|
101
|
+
def initialize platform, release
|
102
|
+
@platform = platform
|
103
|
+
@release = release
|
104
|
+
end
|
105
|
+
# methods never modify inputs.
|
106
|
+
# never set internal state
|
107
|
+
def find_existing_packages package_list
|
108
|
+
return Package.none if package_list.empty?
|
109
|
+
query = Package.where platform: self.platform,
|
110
|
+
release: self.release
|
111
|
+
query.search_qunique_fields(package_list.map(&:uniq_values))
|
112
|
+
end
|
113
|
+
```
|
metadata
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: canson
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jaigouk Kim
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-monads
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-matcher
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.5'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-transaction
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: grpc
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: http-2
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.8.2
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.8.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: http_parser.rb
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.6'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.6'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: dotenv
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: iodine
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.2.3
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.2.3
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rack
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 2.0.1
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 2.0.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: tilt
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 2.0.5
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 2.0.5
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: mustermann
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.4'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.4'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: mime-types
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '2.4'
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '2.4'
|
181
|
+
description: grpc based webframework
|
182
|
+
email:
|
183
|
+
- ping@jaigouk.kim
|
184
|
+
executables:
|
185
|
+
- console
|
186
|
+
- iodine
|
187
|
+
- setup
|
188
|
+
extensions: []
|
189
|
+
extra_rdoc_files: []
|
190
|
+
files:
|
191
|
+
- ".env"
|
192
|
+
- ".gitignore"
|
193
|
+
- ".rubocop.yml"
|
194
|
+
- ".travis.yml"
|
195
|
+
- Gemfile
|
196
|
+
- Guardfile
|
197
|
+
- LICENSE.txt
|
198
|
+
- README.md
|
199
|
+
- Rakefile
|
200
|
+
- bin/console
|
201
|
+
- bin/iodine
|
202
|
+
- bin/setup
|
203
|
+
- canson.gemspec
|
204
|
+
- lib/canson.rb
|
205
|
+
- lib/canson/base.rb
|
206
|
+
- lib/canson/responder.rb
|
207
|
+
- lib/canson/route_map.rb
|
208
|
+
- lib/canson/version.rb
|
209
|
+
- lib/canson/web_socket.rb
|
210
|
+
- refs.md
|
211
|
+
homepage: https://github.com/jaigouk/canson
|
212
|
+
licenses:
|
213
|
+
- MIT
|
214
|
+
metadata: {}
|
215
|
+
post_install_message:
|
216
|
+
rdoc_options: []
|
217
|
+
require_paths:
|
218
|
+
- lib
|
219
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
220
|
+
requirements:
|
221
|
+
- - ">="
|
222
|
+
- !ruby/object:Gem::Version
|
223
|
+
version: '0'
|
224
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
225
|
+
requirements:
|
226
|
+
- - ">="
|
227
|
+
- !ruby/object:Gem::Version
|
228
|
+
version: '0'
|
229
|
+
requirements: []
|
230
|
+
rubyforge_project:
|
231
|
+
rubygems_version: 2.6.7
|
232
|
+
signing_key:
|
233
|
+
specification_version: 4
|
234
|
+
summary: grpc
|
235
|
+
test_files: []
|