minver 0.1.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 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: []