toycol 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1599308369b344cecc36881661fdf60b1ffa8866903939d5ea1380f87ca5e36b
4
- data.tar.gz: b5a67a19b19a0357d13f3a55be989dd462f93a07981f5cb1d443cfd9af19be4c
3
+ metadata.gz: b1b8a58a4fd3f08fc0efe5c808ed00f1fdd78e66b8b0c5467f4deb314f504fde
4
+ data.tar.gz: 86c53cfa48a4331cde4e5888b92a28645bb88b81695b8996b0ef301958364575
5
5
  SHA512:
6
- metadata.gz: 7b9c04a590c8df7f1abfb0ffb7029a3ec55e0a26b4bb4c2fe54dd7b662b792ec1837460c6ba6b21d452bc5c1c0ee8cf5eed3b55b340d594145c4d0996e7a60bb
7
- data.tar.gz: 6be10814f2a7666b7a95ec77a6e737970db8c4f9f238fdb5f0accef6f03eb3cf1c9106401a47e81494fc17ad8393219d3c0f025e778576065f1b86b3eac5db51
6
+ metadata.gz: 6f98b7f6753378032835b787f2f46c1679d6d5df5b88188a1e8f4350ae486146cad37ea1b8f67ec2af0d6f91e8ed24015154a2048f5891d4fb89f662ae912aee
7
+ data.tar.gz: '054893839fdaea8dfb45701e1f2a0cca55c19287e03d18a9266f2547502bb4c18d4fe97b822960aeef208044660e30262bc89945d269a047f3c6398c954c99e8'
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: "[BUG]"
5
+ labels: bug
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ ## Describe the bug
11
+ A clear and concise description of what the bug is.
12
+
13
+ ## To Reproduce
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ ## Expected behavior
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ ## Environment
24
+ - Ruby Version:
25
+ - Rack Version:
26
+ - Puma Version:
27
+
28
+ ## Additional context
29
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: "[FEATURE]"
5
+ labels: enhancement
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ ## Is your feature request related to a problem? Please describe.
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ ## Describe the solution you'd like
14
+ A clear and concise description of what you want to happen.
15
+
16
+ ## Describe alternatives you've considered
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ ## Additional context
20
+ Add any other context or screenshots about the feature request here.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
4
+
1
5
  ## [Unreleased]
2
6
 
3
- ## [0.0.1] - 2021-07-12
7
+ ## [1.0.0] - 2021-07-27
8
+
9
+ The first major release of Toycol.The main features are:
4
10
 
5
- - Initial release
11
+ - [Added] Toycol::Protocol - For defining orignal application protocol
12
+ - [Added] Toycol::Proxy - For accepting requests and pass them to the background server
13
+ - [Added] Toycol::Command - For user friendly CLI
14
+ - `$ toycol server` - For starting proxy & background server
15
+ - `$ toycol client` - For sending request message to server
16
+ - `$ toycol generate` - For generating skeletons of Protocolfile & application
17
+ - [Added] Toycol::Server - As a built-in background server
18
+ - [Added] Rack::Handler::Toycol - For running Rack compartible application & switching background server
data/README.md CHANGED
@@ -1,13 +1,122 @@
1
1
  # Toycol
2
2
 
3
- Toy Application Protocol framework
3
+ Toycol is a small framework for defining toy application protocols.
4
+
5
+ You can define your own application protocol only by writing a parser in Toycol DSL for request messages.
6
+
7
+ Since the server and client programs to run the protocol are built-in from the beginning, you only need to prepare the following two items to run your custom protocol.
8
+ - A configuration file named like `Protocolfile`, `Protocolfile.protocol_name` and so on
9
+ - A Rack compartible application(e.g. `config.ru`)
10
+
11
+ In the real world, there is (yet) no full-fledged web server or browser in the world that runs on the custom protocol you devised.
12
+ Therefore, the protocol defined by this framework is in fact a "toy".
13
+ However, by using this framework, you will be able to experience and learn how the connection between the application layer and the transport layer is, and how the application protocol works on the transport layer.
14
+
15
+ ## Example(on original "Duck" protocol)
16
+
17
+ In this protocol:
18
+ - Client would send message like: `"quack, quack /posts<3user_id=1"`
19
+ - Server would interpret client message: `"GET /posts?user_id=1"`
20
+
21
+ You write your definition in Protocolfile
22
+
23
+ ```ruby
24
+ # Protocolfile.duck (protocol file)
25
+
26
+ Protocol.define(:duck) do
27
+ # [OPTIONAL] You can add your original request methods
28
+ add_request_methods "OTHER"
29
+
30
+ # [OPTIONAL] You can define your custom status codes
31
+ custom_status_codes(
32
+ 600 => "I'm afraid you are not a duck..."
33
+ )
34
+
35
+ # [REQUIRED] Define how you parse request path from request message
36
+ request.path do |message|
37
+ %r{(?<path>\/\w*)}.match(message)[:path]
38
+ end
39
+
40
+ # [REQUIRED] Define how you parse query from request message
41
+ request.query do |message|
42
+ %r{\<3(?<query>.+)}.match(message) { |m| m[:query] }
43
+ end
44
+
45
+ # [REQUIRED] Define how you parse query from request message
46
+ request.http_method do |message|
47
+ case message.scan(/quack/).size
48
+ when 2 then "GET"
49
+ else "OTHER"
50
+ end
51
+ end
52
+ end
53
+ ```
54
+
55
+ Don't forget, you need to prepare your application as well.
56
+
57
+ ```ruby
58
+ # config_duck.ru (Rack compartible application)
59
+
60
+ require "rack"
61
+ require "toycol"
62
+
63
+ # Specify which protocol to use
64
+ Toycol::Protocol.use(:duck)
65
+
66
+ class App
67
+ def call(env)
68
+ case env["REQUEST_METHOD"]
69
+ when "GET"
70
+ [
71
+ 200,
72
+ { "Content-Type" => "text/html" },
73
+ ["Quack, Quack! This app is running by Duck protocol."]
74
+ ]
75
+ when "OTHER"
76
+ [
77
+ 600,
78
+ { "Content-Type" => "text/html" },
79
+ ["Sorry, this application is only for ducks...\n"]
80
+ ]
81
+ end
82
+ end
83
+ end
84
+
85
+ run App.new
86
+ ```
87
+
88
+ Then you run server & client
89
+
90
+ ```
91
+ # In terminal for server
92
+
93
+ $ toycol server config_duck.ru
94
+ Toycol starts build-in server, listening on unix:///tmp/toycol.socket
95
+ Toycol is running on localhost:9292
96
+ => Use Ctrl-C to stop
97
+ ```
98
+
99
+ ```
100
+ # In other terminal for client
101
+
102
+ $ toycol client "quack, quack /posts<3user_id=1"
103
+ [Toycol] Sent request message: quack, quack /posts<3user_id=1
104
+ ---
105
+ [Toycol] Received response message:
106
+
107
+ HTTP/1.1 200 OK
108
+ Content-Type: text/html
109
+ Content-Length: 32
110
+
111
+ Quack, Quack! This app is running by Duck protocol.
112
+ ```
4
113
 
5
114
  ## Installation
6
115
 
7
116
  Add this line to your application's Gemfile:
8
117
 
9
118
  ```ruby
10
- gem 'toycol'
119
+ gem "toycol"
11
120
  ```
12
121
 
13
122
  And then execute:
@@ -20,7 +129,64 @@ Or install it yourself as:
20
129
 
21
130
  ## Usage
22
131
 
23
- WIP
132
+ Toycol provides useful commands to define & run your protocol.
133
+
134
+ #### `toycol generate` - To define your new protocol
135
+
136
+ You can use `toycol generate` command to generate skeletons of Protocolfile and application.
137
+
138
+ ```
139
+ $ toycol generate PROTOCOL_NAME
140
+ ```
141
+
142
+ When you run this command, skeletons of `Protocolfile.PROTOCOL_NAME` and `config_PROTOCOL_NAME.ru` will be generated.
143
+ If you only need one of them, you can specify the type by `-t` option.
144
+
145
+ ```
146
+ $ toycol generate PROTOCOL_NAME -t protocol
147
+ # or
148
+ $ toycol generate PROTOCOL_NAME -t app
149
+ ```
150
+
151
+ If `PROTOCOL_NAME` is not specified, Protocolfile and config.ru will simply be generated.
152
+
153
+ ```
154
+ $ toycol generate
155
+ ```
156
+
157
+ #### `toycol server` - To run server by your protocol
158
+
159
+ After you prepare Protocolfile & application, you need to start server to run the application by `toycol server` command.
160
+
161
+ ```
162
+ # Please specify application file name
163
+ $ toycol server config_`PROTOCOL_NAME`.ru
164
+ ```
165
+
166
+ Then the server will start.
167
+
168
+ Normally, `toycol server` command will start the server built into toycol.
169
+ However, if Puma is already installed in your environment, it will start Puma by default.
170
+
171
+ If you want to explicitly specify which server to use, you can use the -u option.
172
+
173
+ ```
174
+ $ toycol server config_`PROTOCOL_NAME`.ru -u puma
175
+ # or
176
+ $ toycol server config_`PROTOCOL_NAME`.ru -u buid_in
177
+ ```
178
+
179
+ If you would like to check other options, run the command `toycol server -h`.
180
+
181
+ #### `toycol client` - To send request message by your protocol
182
+
183
+ When you would like to send the request message to the server, use `toycol client`.
184
+
185
+ ```
186
+ $ toycol client "YOUR REQUEST MESSAGE"
187
+ ```
188
+
189
+ If you would like to check other options, run the command `toycol client -h`.
24
190
 
25
191
  ## Development
26
192
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "toycol", path: "../../"
@@ -0,0 +1,13 @@
1
+ Toycol::Protocol.define do
2
+ request.path do |message|
3
+ %r{(?<path>\/\w*)}.match(message)[:path]
4
+ end
5
+
6
+ request.query do |message|
7
+ %r{\?(?<query>.+)}.match(message) { |m| m[:query] }
8
+ end
9
+
10
+ request.http_method do |message|
11
+ "GET"
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "toycol"
5
+
6
+ Toycol::Protocol.use
7
+
8
+ class App
9
+ def call(env)
10
+ case env["REQUEST_METHOD"]
11
+ when "GET"
12
+ [
13
+ 200,
14
+ { "Content-Type" => "text/html" },
15
+ ["This app has no protocol name\n"]
16
+ ]
17
+ end
18
+ end
19
+ end
20
+
21
+ run App.new
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  Toycol::Protocol.define(:duck) do
4
2
  custom_status_codes(
5
3
  600 => "I'm afraid you are not a duck..."
@@ -7,11 +5,11 @@ Toycol::Protocol.define(:duck) do
7
5
  additional_request_methods "OTHER"
8
6
 
9
7
  request.path do |message|
10
- %r{(?<path>/\w*)}.match(message)[:path]
8
+ %r{(?<path>\/\w*)}.match(message)[:path]
11
9
  end
12
10
 
13
11
  request.query do |message|
14
- /\?(?<query>.+)/.match(message) { |m| m[:query] }
12
+ %r{\<3(?<query>.+)}.match(message) { |m| m[:query] }
15
13
  end
16
14
 
17
15
  request.http_method do |message|
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
3
  require "rack/handler"
5
4
 
6
5
  module Rack
7
6
  module Handler
8
7
  class Toycol
8
+ extend ::Toycol::Helper
9
+
9
10
  class << self
10
11
  attr_writer :preferred_background_server, :host, :port
11
12
 
@@ -27,18 +28,20 @@ module Rack
27
28
  def select_background_server
28
29
  case @preferred_background_server
29
30
  when "puma"
30
- return "puma" if puma_requireable?
31
+ return "puma" if try_require_puma_handler
31
32
 
32
- puts "Puma is not installed in your environment."
33
- raise LoadError
33
+ raise LoadError, "Puma is not installed in your environment."
34
34
  when nil
35
- puma_requireable? ? "puma" : "build_in"
35
+ try_require_puma_handler ? "puma" : "builtin"
36
36
  else
37
- "build_in"
37
+ "builtin"
38
38
  end
39
+ rescue LoadError
40
+ Process.kill(:INT, Process.ppid)
41
+ abort
39
42
  end
40
43
 
41
- def puma_requireable?
44
+ def try_require_puma_handler
42
45
  require "rack/handler/puma"
43
46
  true
44
47
  rescue LoadError
@@ -48,10 +51,10 @@ module Rack
48
51
  def run_background_server
49
52
  case select_background_server
50
53
  when "puma"
51
- puts "Toycol starts Puma in single mode, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
54
+ logger "Start Puma in single mode, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
52
55
  Rack::Handler::Puma.run(@app, **{ Host: ::Toycol::UNIX_SOCKET_PATH, Silent: true })
53
56
  else
54
- puts "Toycol starts build-in server, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
57
+ logger "Start built-in server, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
55
58
  ::Toycol::Server.run(@app, **{ Path: ::Toycol::UNIX_SOCKET_PATH, Port: @port })
56
59
  end
57
60
  end
data/lib/toycol.rb CHANGED
@@ -1,27 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "optparse"
5
+ require "rack"
6
+ require "socket"
7
+ require "stringio"
4
8
 
5
9
  require_relative "toycol/const"
6
10
  require_relative "toycol/helper"
7
11
  require_relative "toycol/protocol"
8
12
  require_relative "toycol/proxy"
9
13
  require_relative "toycol/server"
10
- require_relative "rack/handler/toycol"
11
-
12
- Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
13
-
14
+ require_relative "toycol/client"
15
+ require_relative "toycol/template_generator"
14
16
  require_relative "toycol/command"
15
17
  require_relative "toycol/version"
16
18
 
19
+ require_relative "rack/handler/toycol"
20
+
17
21
  module Toycol
18
22
  class Error < StandardError; end
19
23
 
20
- class UnauthorizedMethodError < Error; end
24
+ class UnauthorizeError < Error; end
21
25
 
22
- class UnauthorizedRequestError < Error; end
26
+ class UndefinementError < Error; end
23
27
 
24
- class UndefinedRequestMethodError < Error; end
28
+ class DuplicateProtocolError < Error; end
25
29
 
26
- class UnknownStatusCodeError < Error; end
30
+ class HTTPError < Error; end
27
31
  end
32
+
33
+ Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
data/lib/toycol/client.rb CHANGED
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
-
5
3
  module Toycol
6
4
  class Client
5
+ extend Helper
6
+
7
7
  @port = 9292
8
+ @host = "localhost"
8
9
  CHUNK_SIZE = 1024 * 16
9
10
 
10
11
  class << self
11
- attr_writer :port
12
+ attr_writer :port, :host
12
13
 
13
14
  def execute!(request_message, &block)
14
- socket = TCPSocket.new("localhost", @port)
15
+ socket = TCPSocket.new(@host, @port)
15
16
  socket.write(request_message)
16
- puts "[Toycol] Sent request message: #{request_message}\n---"
17
+ logger "Sent request message: #{request_message}\n---"
17
18
 
18
19
  response_message = []
19
20
  response_message << socket.readpartial(CHUNK_SIZE) until socket.eof?
@@ -29,7 +30,7 @@ module Toycol
29
30
 
30
31
  def default_proc
31
32
  proc do |message|
32
- puts "[Toycol] Received response message:\n\n"
33
+ logger "Received response message:\n\n"
33
34
  puts message
34
35
  end
35
36
  end
@@ -1,27 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "optparse"
4
- require_relative "./client"
5
-
6
3
  module Toycol
7
4
  class Command
8
5
  class Options
6
+ @options = {}
7
+
9
8
  class << self
10
9
  def parse!(argv)
11
- options = {}
12
10
  option_parser = create_option_parser
13
11
  sub_command_option_parser = create_sub_command_option_parser
14
12
 
15
13
  begin
16
14
  option_parser.order!(argv)
17
- options[:command] = argv.shift
18
- options[:request_message] = argv.shift if %w[client c].include?(options[:command]) && argv.first != "-h"
19
- sub_command_option_parser[options[:command]].parse!(argv)
15
+ @options[:command] = argv.shift
16
+ @options[:request_message] = argv.shift if request_message?(argv.first)
17
+ @options[:protocol_name] = argv.shift if protocol_name?(argv.first)
18
+ sub_command_option_parser[@options[:command]].parse!(argv)
20
19
  rescue OptionParser::MissingArgument, OptionParser::InvalidOption, ArgumentError => e
21
20
  abort e.message
22
21
  end
23
22
 
24
- options
23
+ @options
25
24
  end
26
25
 
27
26
  def create_option_parser
@@ -45,26 +44,46 @@ module Toycol
45
44
 
46
45
  def create_sub_command_option_parser
47
46
  sub_command_parser = Hash.new { |_k, v| raise ArgumentError, "'#{v}' is not sub command" }
48
- sub_command_parser["client"] = client_option_parser
49
- sub_command_parser["c"] = client_option_parser
50
- sub_command_parser["server"] = server_option_parser
51
- sub_command_parser["s"] = server_option_parser
47
+ sub_command_parser["client"] = client_option_parser
48
+ sub_command_parser["c"] = client_option_parser
49
+ sub_command_parser["server"] = server_option_parser
50
+ sub_command_parser["s"] = server_option_parser
51
+ sub_command_parser["generate"] = generator_option_parser
52
+ sub_command_parser["g"] = generator_option_parser
52
53
  sub_command_parser
53
54
  end
54
55
 
55
56
  private
56
57
 
58
+ def request_message?(arg)
59
+ %w[client c].include?(@options[:command]) \
60
+ && arg != "-p" \
61
+ && arg != "-h"
62
+ end
63
+
64
+ def protocol_name?(arg)
65
+ %w[geberate g].include?(@options[:command]) \
66
+ && arg != "-t" \
67
+ && arg != "-h"
68
+ end
69
+
57
70
  def sub_command_summaries
58
71
  [
59
72
  { name: "client REQUEST_MESSAGE -p PORT", summary: "Send request message to server" },
60
- { name: "server -u SERVER_NAME", summary: "Start proxy and background server" }
73
+ { name: "server -u SERVER_NAME", summary: "Start proxy and background server" },
74
+ { name: "generate NAME -t TYPE", summary: "Generate new protocol or Rack app" }
61
75
  ]
62
76
  end
63
77
 
64
78
  def client_option_parser
65
79
  OptionParser.new do |opt|
66
- opt.on("-p PORT_NUMBER", "--port PORT_NUMBER", "listen on PORT (default: 9292)") do |port|
67
- ::Toycol::Client.port = port
80
+ opt.banner = "Usage: #{opt.program_name} client [-h|--help] REQUEST_MESSAGE [arg...]"
81
+ opt.on("-o HOST", "--host HOST", "connect to HOST (default: localhost)") do |host|
82
+ Client.host = host
83
+ end
84
+
85
+ opt.on("-p PORT_NUMBER", "--port PORT_NUMBER", "connect to PORT (default: 9292)") do |port|
86
+ Client.port = port
68
87
  end
69
88
 
70
89
  opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
@@ -73,6 +92,7 @@ module Toycol
73
92
 
74
93
  def server_option_parser
75
94
  OptionParser.new do |opt|
95
+ opt.banner = "Usage: #{opt.program_name} server [-h|--help] APPLICATION_PATH [arg...]"
76
96
  opt.on("-o HOST", "--host HOST", "bind to HOST (default: localhost)") do |host|
77
97
  ::Rack::Handler::Toycol.host = host
78
98
  end
@@ -81,7 +101,7 @@ module Toycol
81
101
  ::Rack::Handler::Toycol.port = port
82
102
  end
83
103
 
84
- opt.on("-u SERVER_NAME", "--use SERVER_NAME", "switch using SERVER(puma/build_in)") do |server_name|
104
+ opt.on("-u SERVER_NAME", "--use SERVER_NAME", "switch using SERVER(puma/builtin)") do |server_name|
85
105
  ::Rack::Handler::Toycol.preferred_background_server = server_name
86
106
  end
87
107
 
@@ -89,16 +109,14 @@ module Toycol
89
109
  end
90
110
  end
91
111
 
92
- def client_command_help_messages
93
- [
94
- { name: "client -p=PORT_NUMBER", summary: "Send request to server" }
95
- ]
96
- end
112
+ def generator_option_parser
113
+ OptionParser.new do |opt|
114
+ opt.on("-t TYPE", "--type TYPE", "generate TYPE of template (default: :all)") do |type|
115
+ @options[:template_type] = type
116
+ end
97
117
 
98
- def server_command_help_messages
99
- [
100
- { name: "server -u=SERVER_NAME", summary: "Start proxy & background server" }
101
- ]
118
+ opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
119
+ end
102
120
  end
103
121
 
104
122
  def help_command(parser)
@@ -122,10 +140,13 @@ module Toycol
122
140
 
123
141
  case command
124
142
  when "client", "c"
125
- ::Toycol::Client.execute!(options[:request_message])
143
+ Client.execute!(options[:request_message])
126
144
  when "server", "s"
127
145
  ARGV.push("-q", "-s", "toycol")
128
146
  Rack::Server.start
147
+ when "generate", "g"
148
+ type = options[:template_type] || "all"
149
+ TemplateGenerator.generate!(type: type, name: options[:protocol_name])
129
150
  end
130
151
  end
131
152
  end
data/lib/toycol/helper.rb CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Toycol
4
4
  module Helper
5
+ def logger(message)
6
+ puts "[Toycol] #{message}"
7
+ end
8
+
5
9
  private
6
10
 
7
11
  def safe_execution!(&block)
@@ -10,8 +14,8 @@ module Toycol
10
14
 
11
15
  def safe_executionable_tp
12
16
  @safe_executionable_tp ||= TracePoint.new(:script_compiled) do |tp|
13
- if tp.binding.receiver == Toycol::Protocol && tp.method_id.to_s.match?(unauthorized_methods_regex)
14
- raise Toycol::UnauthorizedMethodError, <<~ERROR
17
+ if tp.binding.receiver == Protocol && tp.method_id.to_s.match?(unauthorized_methods_regex)
18
+ raise UnauthorizeError, <<~ERROR
15
19
  - Unauthorized method was called!
16
20
  You can't use methods that may cause injections in your protocol.
17
21
  Ex. Kernel.#eval, Kernel.#exec, Kernel.#require and so on.
@@ -5,23 +5,30 @@ module Toycol
5
5
  class Protocol
6
6
  @definements = {}
7
7
  @protocol_name = nil
8
- @http_status_codes = Toycol::DEFAULT_HTTP_STATUS_CODES.dup
9
- @http_request_methods = Toycol::DEFAULT_HTTP_REQUEST_METHODS.dup
8
+ @http_status_codes = DEFAULT_HTTP_STATUS_CODES.dup
9
+ @http_request_methods = DEFAULT_HTTP_REQUEST_METHODS.dup
10
10
  @custom_status_codes = nil
11
11
  @additional_request_methods = nil
12
12
 
13
13
  class << self
14
- # For protocol definition
15
- def define(protocol_name = nil, &block)
14
+ attr_reader :protocol_name
15
+
16
+ # For Protocolfile to define new protocol
17
+ def define(protocol_name = :default, &block)
18
+ if @definements[protocol_name]
19
+ raise DuplicateProtocolError,
20
+ "#{protocol_name || "Anonymous"} protocol has already been defined"
21
+ end
22
+
16
23
  @definements[protocol_name] = block
17
24
  end
18
25
 
19
- # For application which use the protocol
20
- def use(protocol_name)
26
+ # For application to select which protocol to use
27
+ def use(protocol_name = :default)
21
28
  @protocol_name = protocol_name
22
29
  end
23
30
 
24
- # For server which use the protocol
31
+ # For proxy server to interpret protocol definitions and parse messages
25
32
  def run!(message)
26
33
  @request_message = message.chomp
27
34
 
@@ -67,52 +74,54 @@ module Toycol
67
74
  end
68
75
  end
69
76
 
70
- # For server: Get the request path
77
+ # For proxy server: Fetch the request path
71
78
  def request_path
72
79
  request_path = request.instance_variable_get("@path").call(request_message)
73
80
 
74
- raise UnauthorizedRequestError, "This request path is too long" if request_path.size >= 2048
75
-
76
- if request_path.scan(%r{[/\w\d\-_]}).size < request_path.size
77
- raise UnauthorizedRequestError,
81
+ if request_path.size >= 2048
82
+ raise UnauthorizeError,
83
+ "This request path is too long"
84
+ elsif request_path.scan(%r{[/\w\d\-_]}).size < request_path.size
85
+ raise UnauthorizeError,
78
86
  "This request path contains unauthorized character"
79
87
  end
80
88
 
81
89
  request_path
82
90
  end
83
91
 
84
- # For server: Get the request method
92
+ # For proxy server: Fetch the request method
85
93
  def request_method
86
94
  @http_request_methods.concat @additional_request_methods if @additional_request_methods
87
95
  request_method = request.instance_variable_get("@http_method").call(request_message)
88
96
 
89
97
  unless @http_request_methods.include? request_method
90
- raise UndefinedRequestMethodError, "This request method is undefined"
98
+ raise UndefinementError,
99
+ "This request method is undefined"
91
100
  end
92
101
 
93
102
  request_method
94
103
  end
95
104
 
96
- # For server: Get the query string
105
+ # For proxy server: Fetch the query string
97
106
  def query
98
107
  return unless (parse_query_block = request.instance_variable_get("@query"))
99
108
 
100
109
  parse_query_block.call(request_message)
101
110
  end
102
111
 
103
- # For server: Get the input body
112
+ # For proxy server: Fetch the input body
104
113
  def input
105
114
  return unless (parsed_input_block = request.instance_variable_get("@input"))
106
115
 
107
116
  parsed_input_block.call(request_message)
108
117
  end
109
118
 
110
- # For server: Get the message of status code
119
+ # For proxy server: fetch the message of status code
111
120
  def status_message(status)
112
121
  @http_status_codes.merge!(@custom_status_codes) if @custom_status_codes
113
122
 
114
123
  unless (message = @http_status_codes[status])
115
- raise UnknownStatusCodeError, "Application returns unknown status code"
124
+ raise HTTPError, "Application returns unknown status code"
116
125
  end
117
126
 
118
127
  message
data/lib/toycol/proxy.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
-
5
3
  module Toycol
6
4
  class Proxy
7
5
  include Helper
@@ -13,15 +11,15 @@ module Toycol
13
11
  @path = nil
14
12
  @query = nil
15
13
  @input = nil
16
- @protocol = ::Toycol::Protocol
14
+ @protocol = Protocol
17
15
  @proxy = TCPServer.new(@host, @port)
18
16
  end
19
17
 
20
18
  CHUNK_SIZE = 1024 * 16
21
19
 
22
20
  def start
23
- puts <<~MESSAGE
24
- Toycol is running on #{@host}:#{@port}
21
+ logger <<~MESSAGE
22
+ Start proxy server on #{@protocol.protocol_name} protocol, listening on #{@host}:#{@port}
25
23
  => Use Ctrl-C to stop
26
24
  MESSAGE
27
25
 
@@ -33,13 +31,13 @@ module Toycol
33
31
  while !@client.closed? && !@client.eof?
34
32
  begin
35
33
  request = @client.readpartial(CHUNK_SIZE)
36
- puts "[Toycol] Received message: #{request.inspect.chomp}"
34
+ logger "Received message: #{request.inspect.chomp}"
37
35
 
38
36
  safe_execution! { @protocol.run!(request) }
39
37
  assign_parsed_attributes!
40
38
 
41
39
  http_request_message = build_http_request_message
42
- puts "[Toycol] Message has been translated to HTTP request message: #{http_request_message.inspect}"
40
+ logger "Message has been translated to HTTP request message: #{http_request_message.inspect}"
43
41
  transfer_to_server(http_request_message)
44
42
  rescue StandardError => e
45
43
  puts "#{e.class} #{e.message} - closing socket."
@@ -82,15 +80,15 @@ module Toycol
82
80
  end
83
81
 
84
82
  def transfer_to_server(request_message)
85
- UNIXSocket.open(Toycol::UNIX_SOCKET_PATH) do |server|
83
+ UNIXSocket.open(UNIX_SOCKET_PATH) do |server|
86
84
  server.write request_message
87
85
  server.close_write
88
- puts "[Toycol] Successed to Send HTTP request message to server"
86
+ logger "Successed to Send HTTP request message to server"
89
87
 
90
88
  response_message = []
91
89
  response_message << server.readpartial(CHUNK_SIZE) until server.eof?
92
90
  response_message = response_message.join
93
- puts "[Toycol] Received response message from server: #{response_message.lines.first}"
91
+ logger "Received response message from server: #{response_message.lines.first}"
94
92
 
95
93
  response_line = response_message.lines.first
96
94
  status_number = response_line[9..11]
@@ -98,18 +96,18 @@ module Toycol
98
96
 
99
97
  if (custom_message = @protocol.status_message(status_number.to_i)) != status_message
100
98
  response_message = response_message.sub(status_message, custom_message)
101
- puts "[Toycol] Status message has been translated to custom status message: #{custom_message}"
99
+ logger "Status message has been translated to custom status message: #{custom_message}"
102
100
  end
103
101
 
104
102
  @client.write response_message
105
103
  @client.close_write
106
- puts "[Toycol] Finished to response to client"
104
+ logger "Finished to response to client"
107
105
  server.close
108
106
  end
109
107
  end
110
108
 
111
109
  def shutdown
112
- puts "[Toycol] Catched SIGINT -> Stop to server"
110
+ logger "Caught SIGINT -> Stop to server"
113
111
  exit
114
112
  end
115
113
  end
data/lib/toycol/server.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
-
5
3
  module Toycol
6
4
  class Server
7
5
  BACKLOG = 1024
@@ -50,19 +48,19 @@ module Toycol
50
48
 
51
49
  def default_env
52
50
  {
53
- ::Toycol::PATH_INFO => "",
54
- ::Toycol::QUERY_STRING => "",
55
- ::Toycol::REQUEST_METHOD => "",
56
- ::Toycol::SERVER_NAME => "toycol_server",
57
- ::Toycol::SERVER_PORT => @port.to_s,
58
- ::Toycol::CONTENT_LENGTH => "0",
59
- ::Toycol::RACK_VERSION => Rack::VERSION,
60
- ::Toycol::RACK_INPUT => stringio(""),
61
- ::Toycol::RACK_ERRORS => $stderr,
62
- ::Toycol::RACK_MULTITHREAD => false,
63
- ::Toycol::RACK_MULTIPROCESS => false,
64
- ::Toycol::RACK_RUN_ONCE => false,
65
- ::Toycol::RACK_URL_SCHEME => "http"
51
+ PATH_INFO => "",
52
+ QUERY_STRING => "",
53
+ REQUEST_METHOD => "",
54
+ SERVER_NAME => "toycol_server",
55
+ SERVER_PORT => @port.to_s,
56
+ CONTENT_LENGTH => "0",
57
+ RACK_VERSION => Rack::VERSION,
58
+ RACK_INPUT => stringio(""),
59
+ RACK_ERRORS => $stderr,
60
+ RACK_MULTITHREAD => false,
61
+ RACK_MULTIPROCESS => false,
62
+ RACK_RUN_ONCE => false,
63
+ RACK_URL_SCHEME => "http"
66
64
  }
67
65
  end
68
66
 
@@ -71,7 +69,7 @@ module Toycol
71
69
  end
72
70
 
73
71
  def response_status_code
74
- "HTTP/1.1 #{@returned_status} #{::Toycol::DEFAULT_HTTP_STATUS_CODES[@returned_status.to_i] || "CUSTOM"}\r\n"
72
+ "HTTP/1.1 #{@returned_status} #{DEFAULT_HTTP_STATUS_CODES[@returned_status.to_i] || "CUSTOM"}\r\n"
75
73
  end
76
74
 
77
75
  def response_headers
@@ -108,17 +106,17 @@ module Toycol
108
106
  request_method, request_path, = request_line.split
109
107
  request_path, query_string = request_path.split("?")
110
108
 
111
- @env[::Toycol::REQUEST_METHOD] = request_method
112
- @env[::Toycol::PATH_INFO] = request_path
113
- @env[::Toycol::QUERY_STRING] = query_string || ""
114
- @env[::Toycol::CONTENT_LENGTH]
109
+ @env[REQUEST_METHOD] = request_method
110
+ @env[PATH_INFO] = request_path
111
+ @env[QUERY_STRING] = query_string || ""
112
+ @env[CONTENT_LENGTH]
115
113
 
116
114
  request_headers.each do |request_header|
117
115
  k, v = request_header.split(":").map(&:strip)
118
- @env["::Toycol::#{k.tr("-", "_").upcase}"] = v
116
+ @env[k.tr("-", "_").upcase.to_s] = v
119
117
  end
120
118
 
121
- @env[::Toycol::RACK_INPUT] = stringio(request_body)
119
+ @env[RACK_INPUT] = stringio(request_body)
122
120
  end
123
121
  end
124
122
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toycol
4
+ class TemplateGenerator
5
+ include Helper
6
+
7
+ class << self
8
+ def generate!(type:, name:)
9
+ raise Error, "Unknown Type: This type of template can't be generated" unless valid? type
10
+
11
+ if type == "all"
12
+ new(name, "protocol").generate!
13
+ new(name, "app").generate!
14
+ else
15
+ new(name, type).generate!
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def valid?(type)
22
+ %w[all app protocol].include? type
23
+ end
24
+ end
25
+
26
+ def initialize(name, type)
27
+ @name = name
28
+ @type = type
29
+ end
30
+
31
+ def generate!
32
+ raise Error, "#{filename} already exists" unless Dir.glob(filename).empty?
33
+
34
+ File.open(filename, "w") { |f| f.print template_text_for_new }
35
+ logger "Generate #{filename} in #{FileUtils.pwd}"
36
+ end
37
+
38
+ private
39
+
40
+ def filename
41
+ @filename ||= case @type
42
+ when "protocol" then "Protocolfile#{@name ? ".#{@name}" : nil}"
43
+ when "app" then "config#{@name ? "_#{@name}" : nil}.ru"
44
+ end
45
+ end
46
+
47
+ def template_text_for_new
48
+ if @name
49
+ template_text.sub(":PROTOCOL_NAME", ":#{@name}")
50
+ else
51
+ template_text.sub("\(:PROTOCOL_NAME\)", "")
52
+ end
53
+ end
54
+
55
+ def template_text
56
+ case @type
57
+ when "protocol" then File.open("#{__dir__}/templates/protocol.txt", "r", &:read)
58
+ when "app" then File.open("#{__dir__}/templates/application.txt", "r", &:read)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ require "rack"
2
+ require "toycol"
3
+
4
+ Toycol::Protocol.use(:PROTOCOL_NAME)
5
+
6
+ class App
7
+ def call(env)
8
+ # Define your app on request method, request path, request query etc
9
+ # For example:
10
+ # case env["REQUEST_METHOD"]
11
+ # when "GET"
12
+ # [
13
+ # 200,
14
+ # { "Content-Type" => "text/html" },
15
+ # ["Hello, This app is running by new protocol."]
16
+ # ]
17
+ # when "OTHER"
18
+ # [
19
+ # 600,
20
+ # { "Content-Type" => "text/html" },
21
+ # ["This is response message for additional request method"]
22
+ # ]
23
+ # end
24
+ end
25
+ end
26
+
27
+ run App.new
@@ -0,0 +1,39 @@
1
+ Toycol::Protocol.define(:PROTOCOL_NAME) do
2
+ # For example
3
+ # client would send:
4
+ # quack, quack /posts<3user_id=1
5
+ # server would interpret client message:
6
+ # GET /posts?user_id=1
7
+
8
+
9
+ # [OPTIONAL] You can define your additional request methods:
10
+ # For example:
11
+ # additional_request_methods "OTHER"
12
+
13
+ # [OPTIONAL] You can define your own response status code:
14
+ # For example:
15
+ # custom_status_codes(
16
+ # 600 => "I'm afraid you are not a duck..."
17
+ # )
18
+
19
+ # [REQUIRED] Define how you parse request path from request message
20
+ request.path do |message|
21
+ # For example:
22
+ # %r{(?<path>\/\w*)}.match(message)[:path]
23
+ end
24
+
25
+ # [REQUIRED] Define how you parse query from request message
26
+ request.query do |message|
27
+ # For example:
28
+ # %r{\<3(?<query>.+)}.match(message) { |m| m[:query] }
29
+ end
30
+
31
+ # [REQUIRED] Define how you parse query from request message
32
+ request.http_method do |message|
33
+ # For example:
34
+ # case message.scan(/quack/).size
35
+ # when 2 then "GET"
36
+ # else "OTHER"
37
+ # end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Toycol
4
- VERSION = "0.2.1"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toycol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Misaki Shioi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-23 00:00:00.000000000 Z
11
+ date: 2021-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -32,6 +32,8 @@ executables:
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
36
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
35
37
  - ".github/dependabot.yml"
36
38
  - ".github/workflows/main.yml"
37
39
  - ".gitignore"
@@ -44,6 +46,9 @@ files:
44
46
  - Rakefile
45
47
  - bin/console
46
48
  - bin/setup
49
+ - examples/anonymous/Gemfile
50
+ - examples/anonymous/Protocolfile
51
+ - examples/anonymous/config.ru
47
52
  - examples/duck/Gemfile
48
53
  - examples/duck/Protocolfile.duck
49
54
  - examples/duck/config_duck.ru
@@ -71,6 +76,9 @@ files:
71
76
  - lib/toycol/protocol.rb
72
77
  - lib/toycol/proxy.rb
73
78
  - lib/toycol/server.rb
79
+ - lib/toycol/template_generator.rb
80
+ - lib/toycol/templates/application.txt
81
+ - lib/toycol/templates/protocol.txt
74
82
  - lib/toycol/version.rb
75
83
  - toycol.gemspec
76
84
  homepage: https://github.com/shioimm/toycol