minver 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
+ SHA1:
3
+ metadata.gz: 3d4c1636a148271f9102e1c53bbb4eb9c6404523
4
+ data.tar.gz: 6d8aee43656fb56755daecf1e51c83ff49cc94a0
5
+ SHA512:
6
+ metadata.gz: 9d63a5e430c927c335d5693206a928f6055b1d46869e1cffe739c47cf4216e2d21d030082351b944872636432aa5cda2844699218957d4898354b40397505ebf
7
+ data.tar.gz: fe2978bf5237e338584c54210d06a1dace55f5360b8ee94b2b7707812e0ae8eb07180eab00284c58119a8616b973adc5604c0d008a21c2c360f0000a8d1f85cc
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in minver.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Danyel Bayraktar
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Minver
2
+
3
+ This gem provides a minimal HTTP server solution with two key features:
4
+
5
+ 1. Graceful shutdown of the server
6
+ 2. The caller can retrieve a value that is generated from the route handler
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'minver'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install minver
21
+
22
+ ## Usage
23
+
24
+ This gem allows you to wait for user input via HTTP requests. For example:
25
+
26
+ ```ruby
27
+ require 'minver'
28
+
29
+ # initialize server. default port is 18167, default bind is '::'
30
+ server = Minver::Base.new port: 3000
31
+
32
+ # Define your routes:
33
+
34
+ server.post "/name" do |request|
35
+ name = request.params["name"]
36
+ # pass this param to the caller
37
+ pass name: name
38
+ "Thanks, #{name}, your personal information was submitted."
39
+ end
40
+
41
+ server.post "/age" do |request|
42
+ age = request.params["age"].to_i
43
+ # pass this param to the caller
44
+ if age < 5
45
+ [400, {}, "Hey there, fella. You better ask your parents to use this app instead."]
46
+ else
47
+ pass age: age
48
+ "Thank you, your age has been submitted!"
49
+ end
50
+ end
51
+
52
+ # We're ready to go! Instantiate the hash where we store the info:
53
+
54
+ personal_info = {}
55
+
56
+ # And listen for requests!
57
+
58
+ loop do
59
+ $stdout.puts "Provide your personal information over http://localhost:3000/name and /age"
60
+ personal_info.merge!(server.run)
61
+ break if personal_info.key?(:name) && personal_info.key?(:age)
62
+ end
63
+
64
+ # Now go to your terminal and make a request!
65
+ # e.g.:
66
+ # curl -XPOST -H"Content-Type: application/json" -d'{"name": "Danyel Bayraktar"}' localhost:3000/name
67
+ # or:
68
+ # curl -XPOST -d'age=3' localhost:3000/age
69
+
70
+
71
+ # Do something with this information!
72
+ puts "Hey #{personal_info[:name]}, #{personal_info[:age]} years is the best age to be starring my repo!"
73
+
74
+ # Don't forget to shut down the server
75
+ # (you can also call `stop` instead of `pass` from the route handler while still providing a value):
76
+ server.stop
77
+ ```
78
+
79
+ ## Contributing
80
+
81
+ 1. Fork it ( https://github.com/[my-github-username]/minver/fork )
82
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
83
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
84
+ 4. Push to the branch (`git push origin my-new-feature`)
85
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,145 @@
1
+ require 'socket'
2
+ require 'minver/request'
3
+ require 'minver/response'
4
+ require 'minver/request_error'
5
+
6
+ module Minver
7
+ class Base
8
+
9
+ HTTP_VERSION = "1.1"
10
+ HTTP_METHODS = %w(GET POST PATCH PUT DELETE HEAD)
11
+ HTTP_CODES = {
12
+ 100 => "Continue",
13
+ 101 => "Switching Protocols",
14
+ 103 => "Checkpoint",
15
+ 200 => "OK",
16
+ 201 => "Created",
17
+ 202 => "Accepted",
18
+ 203 => "Non-Authoritative Information",
19
+ 204 => "No Content",
20
+ 205 => "Reset Content",
21
+ 206 => "Partial Content",
22
+ 300 => "Multiple Choices",
23
+ 301 => "Moved Permanently",
24
+ 302 => "Found",
25
+ 303 => "See Other",
26
+ 304 => "Not Modified",
27
+ 306 => "Switch Proxy",
28
+ 307 => "Temporary Redirect",
29
+ 308 => "Resume Incomplete",
30
+ 400 => "Bad Request",
31
+ 401 => "Unauthorized",
32
+ 402 => "Payment Required",
33
+ 403 => "Forbidden",
34
+ 404 => "Not Found",
35
+ 405 => "Method Not Allowed",
36
+ 406 => "Not Acceptable",
37
+ 407 => "Proxy Authentication Required",
38
+ 408 => "Request Timeout",
39
+ 409 => "Conflict",
40
+ 410 => "Gone",
41
+ 411 => "Length Required",
42
+ 412 => "Precondition Failed",
43
+ 413 => "Request Entity Too Large",
44
+ 414 => "Request-URI Too Long",
45
+ 415 => "Unsupported Media Type",
46
+ 416 => "Requested Range Not Satisfiable",
47
+ 417 => "Expectation Failed",
48
+ 422 => "Unprocessable Entity",
49
+ 423 => "Locked",
50
+ 424 => "Failed Dependency",
51
+ 500 => "Internal Server Error",
52
+ 501 => "Not Implemented",
53
+ 502 => "Bad Gateway",
54
+ 503 => "Service Unavailable",
55
+ 504 => "Gateway Timeout",
56
+ 505 => "HTTP Version Not Supported",
57
+ 511 => "Network Authentication Required"
58
+ }
59
+
60
+ def initialize(**options, &block)
61
+ @bind = options.fetch(:bind, '::')
62
+ @port = options.fetch(:port, 18167)
63
+ @clients = []
64
+ instance_eval(&block) if block_given?
65
+ end
66
+
67
+ def server
68
+ @server ||= TCPServer.new @bind, @port
69
+ end
70
+
71
+ def on(method, uri, &block)
72
+ triggers[method][normalize_path(uri)] = block
73
+ end
74
+
75
+ HTTP_METHODS.each do |method|
76
+ define_method method.downcase do |uri, &block|
77
+ on(method, uri, &block)
78
+ end
79
+ end
80
+
81
+ def run(**options)
82
+ $stderr.puts "Listening on #{@bind}:#{@port}." if $DEBUG
83
+ loop do
84
+ result = IO.select([server, *@clients], *([nil, nil, 0] if options[:nonblock]))
85
+ return unless result
86
+ result.first.each do |client|
87
+ # it's possible that "client" here is the server so we extract the client from it.
88
+ @clients << (client = client.accept) if client.respond_to?(:accept)
89
+ if client.eof?
90
+ @clients.delete(client).close
91
+ next
92
+ end
93
+ begin
94
+ request = Request.new(client)
95
+ $stderr.puts request.data.lines.map{|l| "< #{l}"} if $DEBUG
96
+ block = triggers[request.http_method][normalize_path(request.path)]
97
+ response = if block
98
+ begin
99
+ Response.from(instance_exec(request, &block))
100
+ rescue => e
101
+ raise RequestError.new(
102
+ "An error occurred. Check the logs or ask the administrator.",
103
+ 500,
104
+ cause: e
105
+ )
106
+ end
107
+ else
108
+ raise RequestError.new("The resource you were looking for does not exist.", 404)
109
+ end
110
+ if @should_pass
111
+ @should_pass = false
112
+ return @return_value
113
+ end
114
+ rescue RequestError => e
115
+ response = Response.from([e.code, e.headers, e.message])
116
+ raise e.cause || e if (500..599).include? e.code
117
+ ensure
118
+ $stderr.puts response.data.lines.map{|l| "> #{l}"} if $DEBUG
119
+ client.write(response.data)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def stop return_value=nil
126
+ pass return_value
127
+ server.close
128
+ end
129
+
130
+ def pass return_value
131
+ @should_pass = true
132
+ @return_value = return_value
133
+ end
134
+
135
+ protected
136
+
137
+ def normalize_path(path)
138
+ path.squeeze("/").chomp "/"
139
+ end
140
+
141
+ def triggers
142
+ @triggers ||= HTTP_METHODS.inject({}){ |h, m| h.merge(m => {}) }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,72 @@
1
+ require 'stringio'
2
+
3
+ module Minver
4
+ class Parser
5
+ def initialize(stream_or_string)
6
+ @stream = if stream_or_string.is_a? String
7
+ StringIO.new(stream_or_string)
8
+ else
9
+ stream_or_string
10
+ end
11
+ end
12
+
13
+ def http_method
14
+ @http_method ||= request_match[1]
15
+ end
16
+
17
+ def request_url
18
+ @request_url ||= request_match[2]
19
+ end
20
+
21
+ def request_http_version
22
+ @request_http_version ||= request_match[3]
23
+ end
24
+
25
+ def headers
26
+ @headers ||= header_lines.inject({}) do |h, line|
27
+ h.merge(Hash[[line.rstrip.split(': ', 2)]])
28
+ end
29
+ end
30
+
31
+ def [](key)
32
+ headers[key]
33
+ end
34
+
35
+ def data
36
+ @data ||= [*head, body].join
37
+ end
38
+
39
+ def body
40
+ @body ||= @stream.read(headers["Content-Length"].to_i)
41
+ end
42
+
43
+ def path
44
+ @path ||= request_url.split('?')[0]
45
+ end
46
+
47
+ def query_string
48
+ @query_string ||= request_url.split('?')[1] || ''
49
+ end
50
+
51
+ protected
52
+ def request_match
53
+ @request_match ||= request_line.match(/(\S+)\s+(\S+)(?:\s+HTTP\/(\S+)?)/)
54
+ end
55
+
56
+ def request_line
57
+ @request_line ||= head.lines.first
58
+ end
59
+
60
+ def header_lines
61
+ @header_lines ||= head.lines[1...-1]
62
+ end
63
+
64
+ def head
65
+ @head ||= "".tap do |h|
66
+ begin
67
+ h << line = @stream.readline # TODO: non-blocking
68
+ end until ["\r\n", "\n"].include? line
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ require 'minver/parser'
2
+ require 'uri'
3
+ require 'yaml'
4
+
5
+ module Minver
6
+ class Request
7
+ attr_reader :params
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ @params = Hash[URI.decode_www_form(parser.query_string)].tap do |params|
12
+ begin
13
+ puts headers.to_yaml if $DEBUG
14
+ params.merge! case type = headers["Content-Type"]
15
+ when 'application/json'
16
+ require 'json'
17
+ JSON.parse(body)
18
+ when 'application/x-www-form-urlencoded'
19
+ Hash[URI.decode_www_form(body)]
20
+ else
21
+ {}
22
+ end
23
+ rescue => e
24
+ raise RequestError.new(
25
+ "The given content-type is not recognized or the content data is malformed.",
26
+ 400,
27
+ cause: e
28
+ )
29
+ end
30
+ end
31
+ end
32
+
33
+ [:http_method, :headers, :[], :path, :data, :body].each do |method|
34
+ define_method method do
35
+ parser.public_send(method)
36
+ end
37
+ end
38
+
39
+ def parser
40
+ @parser ||= Minver::Parser.new(@client)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ module Minver
2
+ class RequestError < StandardError
3
+ attr_reader :code, :message, :headers, :cause
4
+
5
+ def initialize(message, code, headers: {}, cause: nil)
6
+ super(message)
7
+ @code = code
8
+ @message = message
9
+ @headers = headers
10
+ @cause = cause
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ class Minver::Response
2
+ DEFAULT_HEADERS = {
3
+ "Content-Type" => "text/html; charset=utf-8",
4
+ "Server" => "Minver/1.0",
5
+ "Connection" => "close"
6
+ }
7
+
8
+ def initialize status, headers, body
9
+ headers["Content-Length"] = body.length
10
+ @status = status
11
+ @headers = DEFAULT_HEADERS.merge(
12
+ "Date" => Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z")
13
+ ).merge(headers)
14
+ @body = body
15
+ end
16
+
17
+ def body
18
+ @body
19
+ end
20
+
21
+ def data
22
+ [status_line, *header_lines, '', body].join("\n")
23
+ end
24
+
25
+ def status_line
26
+ ["HTTP/#{Minver::Base::HTTP_VERSION}", @status, Minver::Base::HTTP_CODES[@status]].join(' ')
27
+ end
28
+
29
+ def header_lines
30
+ @headers.map do |k, v|
31
+ [k, v].join(": ")
32
+ end
33
+ end
34
+
35
+ def self.from var
36
+ case var
37
+ when String
38
+ new(200, {}, var)
39
+ when Array
40
+ new(*var)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module Minver
2
+ VERSION = "0.1.0"
3
+ end
data/lib/minver.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Minver; end
2
+
3
+ require 'minver/base'
4
+ require 'minver/version'
data/minver.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'minver/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "minver"
8
+ spec.version = Minver::VERSION
9
+ spec.authors = ["Danyel Bayraktar"]
10
+ spec.email = ["cydrop@gmail.com"]
11
+ spec.summary = %q{Minimal HTTP server with graceful shutdown & value passing}
12
+ spec.homepage = "https://github.com/muja/minver"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.6"
21
+ spec.add_development_dependency "rake"
22
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Danyel Bayraktar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-11-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - cydrop@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - lib/minver.rb
54
+ - lib/minver/base.rb
55
+ - lib/minver/parser.rb
56
+ - lib/minver/request.rb
57
+ - lib/minver/request_error.rb
58
+ - lib/minver/response.rb
59
+ - lib/minver/version.rb
60
+ - minver.gemspec
61
+ homepage: https://github.com/muja/minver
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.5.1
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Minimal HTTP server with graceful shutdown & value passing
85
+ test_files: []