oaf 0.1.12

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.
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