oaf 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ ruby:
3
+ - "1.9.3"
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT LICENSE
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ Oaf
2
+ ---
3
+
4
+ Care-free web app prototyping using files and scripts
5
+
6
+ [![Gem Version](https://badge.fury.io/rb/oaf.png)](http://badge.fury.io/rb/oaf)
7
+ [![Build Status](https://travis-ci.org/ryanuber/oaf.png)](https://travis-ci.org/ryanuber/oaf)
8
+
9
+ `Oaf` provides stupid-easy way of creating dynamic web applications by setting
10
+ all best practices and security considerations aside until you are sure that you
11
+ want to invest your time doing so.
12
+
13
+ `Oaf` was created as a weekend project to create a small, simple HTTP server
14
+ program that uses script execution as its primary mechanism for generating
15
+ responses.
16
+
17
+ Example
18
+ -------
19
+
20
+ Create an executable file named `hello.GET`:
21
+ ```
22
+ #!/bin/bash
23
+ echo "Hello, ${USER}!"
24
+ ```
25
+
26
+ Start the server by running the `oaf` command, then make a request:
27
+ ```
28
+ $ curl localhost:9000/hello
29
+ Hello, ryanuber!
30
+ ```
31
+
32
+ ### Accepted files
33
+ `Oaf` will run *ANY* file with the executable bit set, be it shell, Python, Ruby,
34
+ compiled binary, or whatever else you might have.
35
+
36
+ `Oaf` can also use plain text files.
37
+
38
+ ### How file permissions affect output
39
+ * If the file in your request is executable, the output of its execution will
40
+ be used as the return data.
41
+ * If the file is *NOT* executable, then the contents of the file will be used
42
+ as the return data.
43
+
44
+ ### Nested methods
45
+ You can create nested methods using simple directories. Example:
46
+ ```
47
+ $ ls ./examples/
48
+ hello.GET
49
+
50
+ $ curl http://localhost:8000/examples/hello
51
+ Hello, world!
52
+ ```
53
+
54
+ ### HTTP Methods
55
+ Files must carry the extension of the HTTP method used to invoke them. Some
56
+ examples: `hello.GET`, `hello.POST`, `hello.PUT`, `hello.DELETE`
57
+
58
+ ### Headers and Status
59
+ You can indicate HTTP headers and status using stdout from your script.
60
+
61
+ ```
62
+ #!/bin/bash
63
+ cat <<EOF
64
+ Hello, world!
65
+ ---
66
+ content-type: text/plain
67
+ 200
68
+ EOF
69
+ ```
70
+
71
+ Separated by 3 dashes on a line of their own (`\n---\n`), the very last block
72
+ of output can contain headers and response status.
73
+
74
+ ### Getting request headers and body
75
+ The headers and body of the HTTP request will be passed to the script as
76
+ arguments. The headers will be passed as $1, and the body as $2.
77
+
78
+ ### Catch-all methods
79
+ Catch-all's can be defined by naming a file inside of a directory, beginning and
80
+ ending with underscores (`_`). So for example, `test/_default_.GET` will match:
81
+ `GET /test/anything`, `GET /test/blah`, etc.
82
+
83
+ If you want to define a top-level method for `/test`, you would do so in the
84
+ file at `/test.GET`.
85
+
86
+ Q&A
87
+ ---
88
+ **Why are the headers and status at the bottom of the response?**
89
+ Because it is much easier to echo these last. Since Oaf reads response
90
+ data directly from stdout/stderr, it is very easy for debug or error messages
91
+ to interfere with them. By specifying the headers and status last, we minimize
92
+ the chances of unexpected output damaging the response, as all processing is
93
+ already complete.
94
+
95
+ **Why the name `Oaf`?**
96
+ It's a bit of a clumsy and "oafish" approach at web application prototyping. I
97
+ constantly find myself trying to write server parts of programs before I have
98
+ even completed basic functionality, and sometimes even before I have a clear
99
+ idea of what it is I want the program to do.
100
+
101
+ Acknowledgements
102
+ ----------------
103
+
104
+ `Oaf` is inspired by [Stubb](https://github.com/knuton/stubb). A number of ideas
105
+ and conventions were borrowed from it. Kudos to
106
+ [@knuton](https://github.com/knuton) for having a great idea.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => [:spec]
data/bin/oaf ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/ruby
2
+ # oaf - Care-free web app prototyping using files and scripts
3
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
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.
23
+
24
+ require 'optparse'
25
+ require 'oaf'
26
+
27
+ options = {:port => 9000}
28
+
29
+ begin
30
+ OptionParser.new do |opts|
31
+ opts.banner = [
32
+ 'oaf - Care-free web app prototyping using files and scripts',
33
+ '', 'SYNOPSIS:', ' oaf [options] [path]', '', 'OPTIONS:'].join "\n"
34
+ opts.on('-p', '--port PORT', 'Listening port. Default=9000') do |v|
35
+ if not v.to_i.to_s == v.to_s
36
+ puts "Port number must be an integer"
37
+ exit 1
38
+ end
39
+ options[:port] = v.to_s
40
+ end
41
+ opts.on('--version', 'Show the version number') do
42
+ puts Oaf::VERSION
43
+ exit 0
44
+ end
45
+ opts.on_tail('-h', '--help', 'Show this message') do
46
+ puts opts
47
+ exit 0
48
+ end
49
+ end.parse!
50
+ rescue OptionParser::InvalidOption => e
51
+ puts e.message
52
+ exit 1
53
+ end
54
+
55
+ if ARGV.length == 0
56
+ options[:path] = Dir.pwd
57
+ elsif ARGV.length == 1
58
+ options[:path] = File.expand_path ARGV[0]
59
+ else
60
+ puts "Unknown arguments: #{ARGV[1..ARGV.length].join(' ')}"
61
+ exit 1
62
+ end
63
+
64
+ Oaf::HTTP.serve options[:path], options[:port]
data/lib/oaf.rb ADDED
@@ -0,0 +1,25 @@
1
+ # oaf - Care-free web app prototyping using files and scripts
2
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'oaf/version'
24
+ require 'oaf/util'
25
+ require 'oaf/http'
data/lib/oaf/http.rb ADDED
@@ -0,0 +1,88 @@
1
+ # oaf - Care-free web app prototyping using files and scripts
2
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'webrick'
24
+
25
+ module Oaf
26
+
27
+ module HTTP
28
+ extend Oaf
29
+ extend self
30
+
31
+ # Given output from a script, parse HTTP response details and return them
32
+ # as a hash, including body, status, and headers.
33
+ #
34
+ # == Parameters:
35
+ # output::
36
+ # The output text data returned by a script.
37
+ #
38
+ # == Returns:
39
+ # A hash containing the HTTP response details (body, status, and headers).
40
+ #
41
+ def parse_response output
42
+ has_meta = false
43
+ headers = {'content-type' => 'text/plain'}
44
+ status = 200
45
+ headers, status, meta_size = Oaf::Util.parse_http_meta output
46
+ lines = output.split("\n")
47
+ body = lines.take(lines.length - meta_size).join("\n")+"\n"
48
+ [headers, status, body]
49
+ end
50
+
51
+ # Invokes the Webrick web server library to handle incoming requests, and
52
+ # routes them to the appropriate scripts if they exist on the filesystem.
53
+ #
54
+ # == Parameters:
55
+ # path::
56
+ # The path in which to search for files
57
+ #
58
+ # port::
59
+ # The TCP port to listen on
60
+ #
61
+ def serve path, port
62
+ server = WEBrick::HTTPServer.new :Port => port
63
+ server.mount_proc '/' do |req, res|
64
+ req_body = ''
65
+ req_headers = Oaf::Util.format_request_headers req.header
66
+ if ['POST', 'PUT'].member? req.request_method
67
+ begin
68
+ req_body = req.body
69
+ rescue WEBrick::HTTPStatus::LengthRequired
70
+ true # without this, coverage is not collected.
71
+ end
72
+ else
73
+ req_body = req.query
74
+ end
75
+ file = Oaf::Util.get_request_file path, req.path, req.request_method
76
+ out = Oaf::Util.get_output file, req.header, req_body
77
+ headers, status, body = Oaf::HTTP.parse_response out
78
+ headers.each do |name, value|
79
+ res.header[name] = value
80
+ end
81
+ res.status = status
82
+ res.body = body
83
+ end
84
+ trap 'INT' do server.shutdown end
85
+ server.start
86
+ end
87
+ end
88
+ end
data/lib/oaf/util.rb ADDED
@@ -0,0 +1,200 @@
1
+ # oaf - Care-free web app prototyping using files and scripts
2
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module Oaf
24
+
25
+ module Util
26
+ extend Oaf
27
+ extend self
28
+
29
+ # Determines if a line of output looks anything like an HTTP header
30
+ # declaration.
31
+ #
32
+ # == Parameters:
33
+ # line::
34
+ # A line of text to examine
35
+ #
36
+ # == Returns:
37
+ # A boolean, true if it can be read as a header string, else false.
38
+ #
39
+ def is_http_header? line
40
+ line.split(':').length == 2
41
+ end
42
+
43
+ # Retrieves a hash in the form `name` => `value` from a string that
44
+ # describes an HTTP response header.
45
+ #
46
+ # == Parameters:
47
+ # line::
48
+ # A line of text to parse
49
+ #
50
+ # == Returns
51
+ # A hash in the form `name` => `value`, or `nil`
52
+ #
53
+ def get_http_header line
54
+ return nil if not is_http_header? line
55
+ parts = line.split(':')
56
+ [parts[0].strip, parts[1].strip]
57
+ end
58
+
59
+ # Retrieves the numeric value from a line of text as an HTTP status code.
60
+ #
61
+ # == Parameters:
62
+ # line::
63
+ # A line of text to parse
64
+ #
65
+ # == Returns:
66
+ # An integer if valid, else `nil`.
67
+ #
68
+ def get_http_status line
69
+ is_http_status?(line) ? line.to_i : nil
70
+ end
71
+
72
+ # Determines if an HTTP status code is valid per RFC2616
73
+ #
74
+ # == Parameters:
75
+ # code::
76
+ # A number to validate
77
+ #
78
+ # == Returns
79
+ # A boolean, true if valid, else false.
80
+ #
81
+ def is_http_status? code
82
+ (200..206).to_a.concat((300..307).to_a).concat((400..417).to_a) \
83
+ .concat((500..505).to_a).include? code.to_i
84
+ end
85
+
86
+ # Format a hash of request headers in preparation for passing it to an
87
+ # executable program as an argument.
88
+ #
89
+ # == Parameters:
90
+ # headers::
91
+ # A hash in the form `name` => `value` containing pairs of headers.
92
+ #
93
+ # == Returns:
94
+ # A comma-delimited, colon-separated list of header names and values. The
95
+ # return value of this function should be parsed according to RFC2616.
96
+ #
97
+ def format_request_headers headers
98
+ result = ''
99
+ headers.each do |name, value|
100
+ result += "#{name}:#{value},"
101
+ end
102
+ result.sub!(/,$/, '')
103
+ end
104
+
105
+ # Given an array of text lines, iterate over each of them and determine if
106
+ # they may be interpreted as headers or status. If they can, add them to
107
+ # the result.
108
+ #
109
+ # == Parameters:
110
+ # text::
111
+ # Plain text data to examine
112
+ #
113
+ # == Returns:
114
+ # A 3-item structure containing headers, status, and the number of lines
115
+ # which the complete metadata (including the "---" delimiter) occupies.
116
+ #
117
+ def parse_http_meta text
118
+ headers = {}
119
+ status = 200
120
+ size = 0
121
+ if text.to_s != ''
122
+ parts = text.split /^---$/
123
+ meta = parts.last.split "\n"
124
+ for part in meta
125
+ if Oaf::Util.is_http_header? part
126
+ header, value = Oaf::Util.get_http_header part
127
+ headers.merge! header => value
128
+ elsif Oaf::Util.is_http_status? part
129
+ status = Oaf::Util.get_http_status part
130
+ else
131
+ next
132
+ end
133
+ size += size == 0 ? 2 : 1 # compensate for delimiter
134
+ end
135
+ end
136
+ [headers, status, size]
137
+ end
138
+
139
+ # Return a default response string indicating that nothing could be
140
+ # done and no response files were found.
141
+ #
142
+ # == Returns:
143
+ # A string with response output for parsing.
144
+ #
145
+ def get_default_response
146
+ "oaf: Not Found\n---\n404"
147
+ end
148
+
149
+ # Returns the path to the file to use for the request. If the provided
150
+ # file path does not exist, this method will search for a file within
151
+ # the same directory matching the default convention "_*_".
152
+ #
153
+ # == Parameters:
154
+ # root::
155
+ # The root path to search within.
156
+ # uri::
157
+ # The URI of the request
158
+ # method::
159
+ # The HTTP method of the request
160
+ #
161
+ # == Returns:
162
+ # The path to a file to use, or `nil` if none is found.
163
+ #
164
+ def get_request_file root, uri, method
165
+ file = File.join root, "#{uri}.#{method}"
166
+ if not File.exist? file
167
+ Dir.glob(File.join(File.dirname(file), "_*_.#{method}")).each do |f|
168
+ file = f
169
+ break
170
+ end
171
+ end
172
+ File.exist?(file) ? file : nil
173
+ end
174
+
175
+ # Executes a file, or reads its contents if it is not executable, passing
176
+ # it the request headers and body as arguments, and returns the result.
177
+ #
178
+ # == Parameters:
179
+ # file::
180
+ # The path to the file to use for output
181
+ # headers::
182
+ # The HTTP request headers to pass
183
+ # body::
184
+ # The HTTP request body to pass
185
+ #
186
+ # == Returns:
187
+ # The result from the file, or a default result if the file is not found.
188
+ #
189
+ def get_output file, headers, body
190
+ if file.nil?
191
+ out = Oaf::Util.get_default_response
192
+ elsif File.executable? file
193
+ out = %x(#{file} "#{headers}" "#{body}")
194
+ else
195
+ out = File.open(file).read
196
+ end
197
+ out
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,25 @@
1
+ # oaf - Care-free web app prototyping using files and scripts
2
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module Oaf
24
+ VERSION = '0.1.12'
25
+ end
data/oaf.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'oaf/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'oaf'
7
+ s.version = Oaf::VERSION
8
+ s.summary = 'Web app prototyping'
9
+ s.description = 'Care-free web app prototyping using files and scripts'
10
+ s.authors = ["Ryan Uber"]
11
+ s.email = ['ru@ryanuber.com']
12
+ s.files = %x(git ls-files).split($/)
13
+ s.homepage = 'https://github.com/ryanuber/oaf'
14
+ s.license = 'MIT'
15
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ s.test_files = s.files.grep(%r{^spec/})
17
+ s.require_paths = ['lib']
18
+
19
+ s.required_ruby_version = '>= 1.9'
20
+
21
+ s.add_runtime_dependency 'bundler'
22
+
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'rspec'
25
+ s.add_development_dependency 'simplecov'
26
+ s.add_development_dependency 'coveralls'
27
+ end
@@ -0,0 +1,122 @@
1
+ # oaf - Care-free web app prototyping using files and scripts
2
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'spec_helper'
24
+
25
+ module Oaf
26
+ describe "Returning HTTP Responses" do
27
+ it "should return safe defaults if output is empty" do
28
+ headers, status, body = Oaf::HTTP.parse_response ''
29
+ headers.should eq({})
30
+ status.should eq(200)
31
+ body.should eq("\n")
32
+ end
33
+
34
+ it "should return safe defaults when only body is present" do
35
+ text = "This is a test\n"
36
+ headers, status, body = Oaf::HTTP.parse_response text
37
+ headers.should eq({})
38
+ status.should eq(200)
39
+ body.should eq("This is a test\n")
40
+ end
41
+
42
+ it "should return headers correctly" do
43
+ text = "---\nx-powered-by: oaf"
44
+ headers, status, body = Oaf::HTTP.parse_response text
45
+ headers.should eq({'x-powered-by' => 'oaf'})
46
+ end
47
+
48
+ it "should return status correctly" do
49
+ text = "---\n201"
50
+ headers, status, body = Oaf::HTTP.parse_response text
51
+ status.should eq(201)
52
+ end
53
+
54
+ it "should return body correctly" do
55
+ text = "This is a test\n---\n200"
56
+ headers, status, body = Oaf::HTTP.parse_response text
57
+ body.should eq("This is a test\n")
58
+ end
59
+
60
+ it "should return body correctly when no metadata is present" do
61
+ text = "This is a test"
62
+ headers, status, body = Oaf::HTTP.parse_response text
63
+ body.should eq("This is a test\n")
64
+ end
65
+ end
66
+
67
+ describe "Running an HTTP Server" do
68
+ before(:all) do
69
+ require 'webrick-mocks'
70
+ @tempdir1 = Dir.mktmpdir
71
+ @f1 = Tempfile.new ['oaf', '.GET'], @tempdir1
72
+ @f1.write "This is a test.\n---\n201\nx-powered-by: oaf"
73
+ @f1.close
74
+ @f1request = File.basename(@f1.path).sub!(/\.GET$/, '')
75
+
76
+ @f2 = Tempfile.new ['oaf', '.PUT'], @tempdir1
77
+ @f2.write "Containable Test\n---\n202\nx-powered-by: oaf"
78
+ @f2.close
79
+ @f2request = File.basename(@f2.path).sub!(/\.PUT$/, '')
80
+ end
81
+
82
+ after(:all) do
83
+ @f1.delete
84
+ @f2.delete
85
+ Dir.delete @tempdir1
86
+ end
87
+
88
+ before(:each) do
89
+ @webrick = double()
90
+ @webrick.should_receive(:start).once.and_return(true)
91
+ WEBrick::HTTPServer.stub(:new).and_return(@webrick)
92
+ end
93
+
94
+ it "should start an HTTP server" do
95
+ @webrick.should_receive(:mount_proc).with('/').once \
96
+ .and_yield(Oaf::FakeReq.new, Oaf::FakeRes.new)
97
+ Oaf::HTTP.serve '/tmp', 9000
98
+ end
99
+
100
+ it "should parse the request properly" do
101
+ req = Oaf::FakeReq.new :path => @f1request
102
+ res = Oaf::FakeRes.new
103
+ @webrick.should_receive(:mount_proc).with('/').once \
104
+ .and_yield(req, res)
105
+ Oaf::HTTP.serve @tempdir1, 9000
106
+ res.body.should eq("This is a test.\n")
107
+ res.status.should eq(201)
108
+ res.header.should eq('x-powered-by' => 'oaf')
109
+ end
110
+
111
+ it "should accept containable methods properly" do
112
+ req = Oaf::FakeReq.new({:path => @f2request, :method => 'PUT'})
113
+ res = Oaf::FakeRes.new
114
+ @webrick.should_receive(:mount_proc).with('/').once \
115
+ .and_yield(req, res)
116
+ Oaf::HTTP.serve(@tempdir1, 9000)
117
+ res.body.should eq("Containable Test\n")
118
+ res.status.should eq(202)
119
+ res.header.should eq('x-powered-by' => 'oaf')
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,208 @@
1
+ # oaf - Care-free web app prototyping using files and scripts
2
+ # Copyright 2013 Ryan Uber <ru@ryanuber.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'spec_helper'
24
+
25
+ module Oaf
26
+ describe "Header Lines" do
27
+ it "should detect a valid header line" do
28
+ result = Oaf::Util.is_http_header? 'x-powered-by: oaf'
29
+ result.should be_true
30
+ end
31
+
32
+ it "should detect an invalid header line" do
33
+ result = Oaf::Util.is_http_header? 'invalid header line'
34
+ result.should be_false
35
+ end
36
+
37
+ it "should parse a name and value from a header line" do
38
+ result = Oaf::Util.get_http_header 'x-powered-by: oaf'
39
+ result.should eq(['x-powered-by', 'oaf'])
40
+ end
41
+
42
+ it "should detect an invalid header line during header parsing" do
43
+ result = Oaf::Util.get_http_header 'invalid header line'
44
+ result.should be_nil
45
+ end
46
+ end
47
+
48
+ describe "Status Lines" do
49
+ it "should detect all valid HTTP status codes" do
50
+ [200, 201, 202, 203, 204, 205, 206,
51
+ 300, 301, 302, 303, 304, 305, 306, 307,
52
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411,
53
+ 412, 413, 414, 415, 416, 417,
54
+ 500, 501, 502, 503, 504, 505].each do |status|
55
+ result = Oaf::Util.is_http_status? status
56
+ result.should be_true
57
+ end
58
+ end
59
+
60
+ it "should be able to validate a string http status line" do
61
+ result = Oaf::Util.is_http_status? '200'
62
+ result.should be_true
63
+ end
64
+
65
+ it "should detect an invalid http status code" do
66
+ result1 = Oaf::Util.is_http_status? 199
67
+ result2 = Oaf::Util.is_http_status? 'not a status'
68
+ result1.should be_false
69
+ result2.should be_false
70
+ end
71
+
72
+ it "should retrieve an http status code" do
73
+ result = Oaf::Util.get_http_status '200'
74
+ result.should eq(200)
75
+ end
76
+
77
+ it "should detect an invalid status code during parsing" do
78
+ result = Oaf::Util.get_http_status 'not a status'
79
+ result.should be_nil
80
+ end
81
+ end
82
+
83
+ describe "Format Request Headers" do
84
+ it "should return a single key/value for just one header" do
85
+ headers = [['x-powered-by', 'oaf']]
86
+ result = Oaf::Util.format_request_headers headers
87
+ result.should eq('x-powered-by:oaf')
88
+ end
89
+
90
+ it "should return a comma-delimited list for multiple headers" do
91
+ headers = [['x-powered-by', 'oaf'], ['content-type', 'text/plain']]
92
+ result = Oaf::Util.format_request_headers headers
93
+ result.should eq('x-powered-by:oaf,content-type:text/plain')
94
+ end
95
+
96
+ it "should return nil if no headers present" do
97
+ headers = []
98
+ result = Oaf::Util.format_request_headers headers
99
+ result.should be_nil
100
+ end
101
+ end
102
+
103
+ describe "Parse Request Metadata From Output" do
104
+ it "should find headers in request metadata" do
105
+ text = ['---', 'x-powered-by: oaf', '201'].join "\n"
106
+ headers, status, size = Oaf::Util.parse_http_meta text
107
+ headers.should eq('x-powered-by' => 'oaf')
108
+ end
109
+
110
+ it "should return the number of lines the metadata consumes" do
111
+ text = ['---', 'x-powered-by: oaf', '200'].join "\n"
112
+ headers, status, size = Oaf::Util.parse_http_meta text
113
+ size.should eq(3)
114
+ end
115
+
116
+ it "should assume 200 as the default return code" do
117
+ text = ['---', 'x-powered-by: oaf'].join "\n"
118
+ headers, status, size = Oaf::Util.parse_http_meta text
119
+ status.should eq(200)
120
+ end
121
+
122
+ it "should assume meta size 0 if no metadata is present" do
123
+ text = 'this response uses default metadata'
124
+ headers, status, size = Oaf::Util.parse_http_meta text
125
+ size.should eq(0)
126
+ end
127
+
128
+ it "should return safe defaults if the response is empty" do
129
+ text = ''
130
+ headers, status, size = Oaf::Util.parse_http_meta text
131
+ headers.should eq({})
132
+ status.should eq(200)
133
+ size.should eq(0)
134
+ end
135
+ end
136
+
137
+ describe "Determine File Paths" do
138
+ before(:all) do
139
+ @tempdir1 = Dir.mktmpdir
140
+ @tempdir2 = Dir.mktmpdir
141
+
142
+ @f1 = Tempfile.new ['oaf', '.GET'], @tempdir1
143
+ @f1.write "This is a test.\n"
144
+ @f1.close
145
+ @f1request = File.basename(@f1.path).sub!(/\.GET$/, '')
146
+
147
+ @f2 = Tempfile.new ['_', '_.GET'], @tempdir1
148
+ @f2.write "This is a default file.\n"
149
+ @f2.close
150
+ end
151
+
152
+ after(:all) do
153
+ @f1.delete
154
+ @f2.delete
155
+ Dir.delete @tempdir1
156
+ Dir.delete @tempdir2
157
+ end
158
+
159
+ it "should find existing files correctly" do
160
+ result = Oaf::Util.get_request_file @tempdir1, @f1request, 'GET'
161
+ result.should eq(@f1.path)
162
+ end
163
+
164
+ it "should return the fall-through file if request file doesn't exist" do
165
+ result = Oaf::Util.get_request_file @tempdir1, 'nonexistent', 'GET'
166
+ result.should eq(@f2.path)
167
+ end
168
+
169
+ it "should return nil if neither the requested or default file exist" do
170
+ result = Oaf::Util.get_request_file @tempdir2, 'nonexistent', 'GET'
171
+ result.should be_nil
172
+ end
173
+ end
174
+
175
+ describe "Executing and Reading Files" do
176
+ before(:all) do
177
+ @f1 = Tempfile.new 'oaf'
178
+ @f1.chmod 0755
179
+ @f1.write "#!/bin/bash\necho 'This is a test'"
180
+ @f1.close
181
+
182
+ @f2 = Tempfile.new 'oaf'
183
+ @f2.chmod 0644
184
+ @f2.write "This is a test\n"
185
+ @f2.close
186
+ end
187
+
188
+ after(:all) do
189
+ @f1.delete
190
+ @f2.delete
191
+ end
192
+
193
+ it "should execute a file if it is executable" do
194
+ result = Oaf::Util.get_output @f1.path, nil, nil
195
+ result.should eq("This is a test\n")
196
+ end
197
+
198
+ it "should read file contents if it is not executable" do
199
+ result = Oaf::Util.get_output @f2.path, nil, nil
200
+ result.should eq("This is a test\n")
201
+ end
202
+
203
+ it "should assume safe defaults if the file doesnt exist" do
204
+ result = Oaf::Util.get_output nil, nil, nil
205
+ result.should eq(Oaf::Util.get_default_response)
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,7 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter '/spec/'
4
+ end
5
+ require 'oaf'
6
+ require 'ostruct'
7
+ require 'tempfile'
@@ -0,0 +1,46 @@
1
+ require 'webrick'
2
+
3
+ module Oaf
4
+ class FakeReq
5
+
6
+ attr_accessor :path, :request_method, :header, :query
7
+ attr_writer :body
8
+
9
+ @body = @path = @request_method = @query = nil
10
+ @header = Hash.new
11
+
12
+ def initialize opts={}
13
+ @body = opts[:body] ? opts[:body] : nil
14
+ @path, @query = opts[:path] ? opts[:path].split('?') : ['/']
15
+ @request_method = opts[:method] ? opts[:method] : 'GET'
16
+ @header = opts[:header] ? opts[:header] : Hash.new
17
+ end
18
+
19
+ def body
20
+ if ['POST', 'PUT'].member? @request_method
21
+ # Mock a webrick bug
22
+ raise WEBrick::HTTPStatus::LengthRequired
23
+ end
24
+ @body
25
+ end
26
+ end
27
+
28
+ class FakeRes
29
+
30
+ attr_accessor :body, :status
31
+ attr_reader :header
32
+
33
+ def initialize
34
+ @body = @status = nil
35
+ @header = Hash.new
36
+ end
37
+
38
+ def [](field)
39
+ @header[field]
40
+ end
41
+
42
+ def []=(field, value)
43
+ @header[field] = value
44
+ end
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oaf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.12
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Uber
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: &21830760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *21830760
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &21829320 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *21829320
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &21828360 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *21828360
47
+ - !ruby/object:Gem::Dependency
48
+ name: simplecov
49
+ requirement: &21825800 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *21825800
58
+ - !ruby/object:Gem::Dependency
59
+ name: coveralls
60
+ requirement: &21825300 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *21825300
69
+ description: Care-free web app prototyping using files and scripts
70
+ email:
71
+ - ru@ryanuber.com
72
+ executables:
73
+ - oaf
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .travis.yml
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - bin/oaf
83
+ - lib/oaf.rb
84
+ - lib/oaf/http.rb
85
+ - lib/oaf/util.rb
86
+ - lib/oaf/version.rb
87
+ - oaf.gemspec
88
+ - spec/oaf/http_spec.rb
89
+ - spec/oaf/util_spec.rb
90
+ - spec/spec_helper.rb
91
+ - spec/webrick-mocks.rb
92
+ homepage: https://github.com/ryanuber/oaf
93
+ licenses:
94
+ - MIT
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '1.9'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 1.8.11
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Web app prototyping
117
+ test_files:
118
+ - spec/oaf/http_spec.rb
119
+ - spec/oaf/util_spec.rb
120
+ - spec/spec_helper.rb
121
+ - spec/webrick-mocks.rb