hyperdrive 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +3 -0
- data/Gemfile +2 -1
- data/README.md +75 -2
- data/bin/console +14 -0
- data/bin/hyperdrive +42 -0
- data/hyperdrive.gemspec +6 -5
- data/lib/hyperdrive/docs.rb +76 -0
- data/lib/hyperdrive/dsl/main.rb +19 -0
- data/lib/hyperdrive/dsl/resource.rb +42 -0
- data/lib/hyperdrive/dsl.rb +9 -0
- data/lib/hyperdrive/errors/bad_request.rb +13 -0
- data/lib/hyperdrive/errors/dsl/unknown_argument.rb +21 -0
- data/lib/hyperdrive/errors/http_error.rb +11 -0
- data/lib/hyperdrive/errors/internal_server_error.rb +13 -0
- data/lib/hyperdrive/errors/method_not_allowed.rb +17 -0
- data/lib/hyperdrive/errors/not_found.rb +13 -0
- data/lib/hyperdrive/errors/not_implemented.rb +17 -0
- data/lib/hyperdrive/errors/unauthorized.rb +13 -0
- data/lib/hyperdrive/errors.rb +10 -0
- data/lib/hyperdrive/resource.rb +29 -3
- data/lib/hyperdrive/response.rb +41 -0
- data/lib/hyperdrive/server.rb +30 -0
- data/lib/hyperdrive/values.rb +31 -0
- data/lib/hyperdrive/version.rb +1 -1
- data/lib/hyperdrive.rb +12 -4
- data/spec/hyperdrive/docs_spec.rb +56 -0
- data/spec/hyperdrive/dsl/main_spec.rb +16 -0
- data/spec/hyperdrive/dsl/resource_spec.rb +58 -0
- data/spec/hyperdrive/resource_spec.rb +29 -5
- data/spec/hyperdrive/response_spec.rb +23 -0
- data/spec/hyperdrive/server_spec.rb +11 -0
- data/spec/spec_helper.rb +21 -2
- metadata +77 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04f0142457c2c2727dc798316907630f2d53c828
|
4
|
+
data.tar.gz: b1742f5f51b354d1ada859f1b04603e1703396b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 165eb035a20d31e3c33c29e82dc71cd856661aa91d58f0b542e5e90bfe700f185dd1262ea6714e203cbc5db1f047b0d5f80e929ac8da491bbca6fb7fa74dd7eb
|
7
|
+
data.tar.gz: f2b83b37947c9b2bd3ed27c6c7db5b5deb15729714b324c68a6c28dc0520ddb13b28f5ac3f40ae9d1a98e117705b15a1aefa344b72c1fec360d856a5c279bc2d
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -3,6 +3,7 @@ gemspec
|
|
3
3
|
|
4
4
|
group :development do
|
5
5
|
gem 'gem-release', require: false
|
6
|
+
gem 'pry', require: false
|
6
7
|
end
|
7
8
|
|
8
9
|
group :test, :rake do
|
@@ -17,5 +18,5 @@ group :test do
|
|
17
18
|
gem 'minitest', require: false
|
18
19
|
gem 'minitest-spec-context', require: false
|
19
20
|
gem 'minitest-reporters', require: false
|
20
|
-
|
21
|
+
gem 'rack-test', require: false
|
21
22
|
end
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Hyperdrive
|
2
2
|
|
3
|
-
Ruby DSL for defining self-documenting,
|
3
|
+
Ruby DSL for defining self-documenting, HATEOAS™ complaint, Hypermedia endpoints.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -21,6 +21,7 @@ Or install it yourself as:
|
|
21
21
|
Proposed Syntax (WIP):
|
22
22
|
|
23
23
|
```ruby
|
24
|
+
# api.rb
|
24
25
|
hyperdrive do
|
25
26
|
resource(:thing) do
|
26
27
|
name 'Thing Resource'
|
@@ -40,13 +41,85 @@ hyperdrive do
|
|
40
41
|
# applied). Unlike allowed params, filters are not required by default.
|
41
42
|
filter :start_date, 'Format: YYYY-MM-DD'
|
42
43
|
filter :end_date, 'Format: YYYY-MM-DD'
|
43
|
-
filter :parent_id, 'Parent ID of Thing
|
44
|
+
filter :parent_id, 'Parent ID of Thing', required: true
|
45
|
+
|
46
|
+
|
47
|
+
# Handle API Requests
|
48
|
+
#
|
49
|
+
# - Simply return a string to be used as the body of the response. How you generate
|
50
|
+
# that string is completely up to you.
|
51
|
+
# - Status Codes and Headers will be handled automatically.
|
52
|
+
# - Any unhandled requests will return a 405 "Method Not Supported" error
|
53
|
+
|
54
|
+
request(:get) do
|
55
|
+
# retrieve 1-N objects...
|
56
|
+
'{ ... }'
|
57
|
+
end
|
58
|
+
|
59
|
+
# Options and Head requests will be handled automatically and
|
60
|
+
# will use info from the GET request block you define, as needed.
|
61
|
+
|
62
|
+
# POST Requests should return the full object that was created during the request.
|
63
|
+
request(:post) do
|
64
|
+
# create the object...
|
65
|
+
'{ ... }'
|
66
|
+
end
|
67
|
+
|
68
|
+
# PUT Requests should return the full object that was updated during the request.
|
69
|
+
request(:put) do
|
70
|
+
# 'upsert' the object...
|
71
|
+
'{ ... }'
|
72
|
+
end
|
73
|
+
|
74
|
+
# PATCH requests should return the full object that was updated during the request.
|
75
|
+
request(:patch) do
|
76
|
+
# update the object...
|
77
|
+
'{ ... }'
|
78
|
+
end
|
79
|
+
|
80
|
+
# DELETE requests should return a simple response indicating success.
|
81
|
+
request(:delete) do
|
82
|
+
# delete the object...
|
83
|
+
'{"deleted":true}'
|
84
|
+
end
|
44
85
|
end
|
45
86
|
end
|
87
|
+
|
88
|
+
# config.ru
|
89
|
+
run Hyperdrive::Server
|
46
90
|
```
|
47
91
|
|
92
|
+
## CLI
|
93
|
+
|
94
|
+
### Generating Documentation
|
95
|
+
|
96
|
+
`$ hyperdrive docs <option> <parameter>`
|
97
|
+
|
98
|
+
__`--input` Option__
|
99
|
+
|
100
|
+
Use the `--input` option and specify a file or directory as a parameter to generate documentation for your API resources.
|
101
|
+
|
102
|
+
- `$ hyperdrive docs --input api.rb`
|
103
|
+
|
104
|
+
or
|
105
|
+
|
106
|
+
- `$ hyperdrive docs --input api`
|
107
|
+
|
108
|
+
`-in` can be used as an alias for `--input`
|
109
|
+
|
110
|
+
__`--output` Option__
|
111
|
+
|
112
|
+
You can also provide a `--output` option and specify a destination for your documentation to be created.
|
113
|
+
|
114
|
+
- `$ hyperdrive docs --input api.rb --output docs/docs.md`
|
115
|
+
|
116
|
+
`-out` can be used as an alias for `--output`
|
117
|
+
|
118
|
+
If the `--output` option is not provided the generated documentation will be written to `docs/doc.md` by default.
|
119
|
+
|
48
120
|
## Project Status
|
49
121
|
|
122
|
+
- Version: [![Gem Version](https://badge.fury.io/rb/hyperdrive.png)](http://badge.fury.io/rb/hyperdrive)
|
50
123
|
- Build: [![Build Status](https://secure.travis-ci.org/styleseek/hyperdrive.png?branch=master)](https://travis-ci.org/styleseek/hyperdrive)
|
51
124
|
- Code Quality: [![Code Climate](https://codeclimate.com/github/styleseek/hyperdrive.png)](https://codeclimate.com/github/styleseek/hyperdrive)
|
52
125
|
- Dependencies: [![Dependency Status](https://gemnasium.com/styleseek/hyperdrive.png)](https://gemnasium.com/styleseek/hyperdrive)
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
ENV['RACK_ENV'] ||= 'development'
|
3
|
+
# load path
|
4
|
+
lib_path = File.expand_path('../../lib', __FILE__)
|
5
|
+
($:.unshift lib_path) unless ($:.include? lib_path)
|
6
|
+
|
7
|
+
# require farm
|
8
|
+
require 'bundler'
|
9
|
+
Bundler.require(:default, ENV['RACK_ENV'])
|
10
|
+
require 'pry'
|
11
|
+
require 'hyperdrive'
|
12
|
+
|
13
|
+
# fire up the ftl drive
|
14
|
+
Pry.start
|
data/bin/hyperdrive
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
ENV['RACK_ENV'] ||= 'development'
|
3
|
+
# load path
|
4
|
+
lib_path = File.expand_path('../../lib', __FILE__)
|
5
|
+
($:.unshift lib_path) unless ($:.include? lib_path)
|
6
|
+
|
7
|
+
require 'thor'
|
8
|
+
require 'hyperdrive'
|
9
|
+
|
10
|
+
module Hyperdrive
|
11
|
+
class CLI < Thor
|
12
|
+
include Thor::Actions
|
13
|
+
|
14
|
+
desc "docs", "Generate docs in markdown based on your resources."
|
15
|
+
method_option :input, aliases: "\--in", desc: "Specify the directory or file to generate docs for."
|
16
|
+
method_option :output, aliases: "\--out", desc: "Specify the destination of the docs.", default: "docs/api.md"
|
17
|
+
def docs
|
18
|
+
input = File.expand_path(options[:input])
|
19
|
+
|
20
|
+
if File.file?(input)
|
21
|
+
require input
|
22
|
+
elsif File.directory?(input)
|
23
|
+
Dir.glob("#{input}/*.rb").each do |file|
|
24
|
+
require file
|
25
|
+
end
|
26
|
+
else
|
27
|
+
raise ArgumentError, "Input is neither a file nor a directory"
|
28
|
+
end
|
29
|
+
|
30
|
+
if hyperdrive.resources.empty?
|
31
|
+
say "This API doesn't have any resources to document", :red
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
data = Hyperdrive::Docs.new(hyperdrive.resources).output
|
36
|
+
create_file(options[:output], data)
|
37
|
+
say "Done!"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Hyperdrive::CLI.start
|
data/hyperdrive.gemspec
CHANGED
@@ -9,15 +9,16 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.authors = ['StyleSeek Engineering']
|
10
10
|
gem.email = ['engineering@styleseek.com']
|
11
11
|
gem.summary = %q{Hypermedia State Machine}
|
12
|
-
gem.description = %q{Ruby DSL for defining self-documenting,
|
12
|
+
gem.description = %q{Ruby DSL for defining self-documenting, HATEOAS™ complaint, Hypermedia API endpoints.}
|
13
13
|
gem.homepage = "https://github.com/styleseek/hyperdrive"
|
14
14
|
gem.license = "MIT"
|
15
15
|
|
16
16
|
gem.files = `git ls-files -z`.split("\x0")
|
17
|
-
gem.executables =
|
17
|
+
gem.executables = ['hyperdrive']
|
18
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
19
|
gem.require_paths = ["lib"]
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
|
21
|
+
gem.add_dependency 'rack'
|
22
|
+
gem.add_dependency 'linguistics'
|
23
|
+
gem.add_dependency 'thor'
|
23
24
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
class Docs
|
5
|
+
attr_reader :resources
|
6
|
+
|
7
|
+
def initialize(resources)
|
8
|
+
@resources = resources
|
9
|
+
end
|
10
|
+
|
11
|
+
def output
|
12
|
+
out = ""
|
13
|
+
resources.each_value do |resource|
|
14
|
+
out += header(resource.name)
|
15
|
+
out += paragraph(resource.desc)
|
16
|
+
out += header("Endpoint URL", 2)
|
17
|
+
out += paragraph(bullet(code(resource.endpoint), 1))
|
18
|
+
out += header("Params", 2)
|
19
|
+
out += list(resource.allowed_params)
|
20
|
+
out += header("Filters", 2)
|
21
|
+
out += list(resource.filters)
|
22
|
+
end
|
23
|
+
out
|
24
|
+
end
|
25
|
+
|
26
|
+
def header(string, level = 1)
|
27
|
+
raise ArgumentError, "Header level must be between 1 and 6." unless (1..6).cover?(level)
|
28
|
+
header = "#" * level
|
29
|
+
"\n\n#{header} #{string}\n\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
def paragraph(string)
|
33
|
+
"#{string}\n\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
def bold(string)
|
37
|
+
"__#{string}__"
|
38
|
+
end
|
39
|
+
|
40
|
+
def italics(string)
|
41
|
+
"_#{string}_"
|
42
|
+
end
|
43
|
+
|
44
|
+
def code(string)
|
45
|
+
"`#{string}`"
|
46
|
+
end
|
47
|
+
|
48
|
+
def bullet(string, nest = 1)
|
49
|
+
raise ArgumentError, "Nest level must be between 1 and 3." unless (1..3).cover?(nest)
|
50
|
+
nest = " " * nest
|
51
|
+
"#{nest}- #{string}\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
def list(items)
|
55
|
+
list = ""
|
56
|
+
items.each do |key, value|
|
57
|
+
list += bullet(bold(key), 1)
|
58
|
+
|
59
|
+
value.each do |subkey, subvalue|
|
60
|
+
list += bullet(italics(subkey), 2)
|
61
|
+
|
62
|
+
if subvalue.kind_of? Array
|
63
|
+
list += bullet(code_options(subvalue), 3)
|
64
|
+
else
|
65
|
+
list += bullet(subvalue, 3)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
list
|
70
|
+
end
|
71
|
+
|
72
|
+
def code_options(options)
|
73
|
+
options.map { |option| code(option) }.join(", ")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Hyperdrive
|
5
|
+
module DSL
|
6
|
+
class Main
|
7
|
+
include Singleton
|
8
|
+
attr_reader :resources
|
9
|
+
|
10
|
+
def initialize(&block)
|
11
|
+
@resources = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def resource(name, &block)
|
15
|
+
@resources[name] = Resource.new(name, &block).resource
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module DSL
|
5
|
+
class Resource
|
6
|
+
attr_reader :resource
|
7
|
+
def initialize(key, &block)
|
8
|
+
@resource = ::Hyperdrive::Resource.new(key)
|
9
|
+
instance_eval(&block) if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def name(name)
|
13
|
+
resource.name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def desc(description)
|
17
|
+
resource.desc = description
|
18
|
+
end
|
19
|
+
|
20
|
+
def param(*args)
|
21
|
+
resource.register_param(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def filter(*args)
|
25
|
+
resource.register_filter(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def request(method, &block)
|
29
|
+
unless definable_request_methods.include? method
|
30
|
+
raise Errors::DSL::UnknownArgument.new(method, 'request')
|
31
|
+
end
|
32
|
+
resource.define_request_handler(method, block)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def definable_request_methods
|
38
|
+
[:get, :post, :put, :patch, :delete].freeze
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Hyperdrive
|
2
|
+
module Errors
|
3
|
+
module DSL
|
4
|
+
class UnknownArgument < RuntimeError
|
5
|
+
def initialize(argument, method_name)
|
6
|
+
@argument = case argument
|
7
|
+
when Symbol
|
8
|
+
":#{argument}"
|
9
|
+
else
|
10
|
+
argument.to_s
|
11
|
+
end
|
12
|
+
@method_name = method_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
"#{@argument} is not supported by `#{@method_name}'."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Hyperdrive
|
2
|
+
module Errors
|
3
|
+
class MethodNotAllowed < HTTPError
|
4
|
+
def initialize(request_method)
|
5
|
+
@request_method = request_method
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
"A request was made using a request method (#{@request_method}) not supported by this resource."
|
10
|
+
end
|
11
|
+
|
12
|
+
def http_status_code
|
13
|
+
405
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Hyperdrive
|
2
|
+
module Errors
|
3
|
+
class NotImplemented < HTTPError
|
4
|
+
def initialize(request_method)
|
5
|
+
@request_method = request_method
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
"The server either does not recognise the request method (#{@request_method}), or it lacks the ability to fulfill the request."
|
10
|
+
end
|
11
|
+
|
12
|
+
def http_status_code
|
13
|
+
501
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'hyperdrive/errors/http_error'
|
4
|
+
require 'hyperdrive/errors/bad_request'
|
5
|
+
require 'hyperdrive/errors/internal_server_error'
|
6
|
+
require 'hyperdrive/errors/method_not_allowed'
|
7
|
+
require 'hyperdrive/errors/not_found'
|
8
|
+
require 'hyperdrive/errors/not_implemented'
|
9
|
+
require 'hyperdrive/errors/unauthorized'
|
10
|
+
require 'hyperdrive/errors/dsl/unknown_argument'
|
data/lib/hyperdrive/resource.rb
CHANGED
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
module Hyperdrive
|
4
4
|
class Resource
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :endpoint, :allowed_params, :filters, :request_handlers
|
6
6
|
attr_accessor :name, :desc
|
7
7
|
|
8
|
-
def initialize(
|
9
|
-
@
|
8
|
+
def initialize(key)
|
9
|
+
@key = key
|
10
|
+
@endpoint = "/#{@key.to_s.en.plural}"
|
10
11
|
@allowed_params = default_allowed_params
|
11
12
|
@filters = default_filters
|
13
|
+
@request_handlers = default_request_handlers
|
12
14
|
end
|
13
15
|
|
14
16
|
def register_param(key, description, options = {})
|
@@ -21,6 +23,26 @@ module Hyperdrive
|
|
21
23
|
@filters[key] = { desc: description }.merge(options)
|
22
24
|
end
|
23
25
|
|
26
|
+
def define_request_handler(request_method, block)
|
27
|
+
@request_handlers[request_method] = block
|
28
|
+
if request_method == :get
|
29
|
+
@request_handlers[:head] = block
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_handler(http_request_method)
|
34
|
+
request_method = Hyperdrive::Values.request_methods_string_map[http_request_method]
|
35
|
+
request_handlers[request_method]
|
36
|
+
end
|
37
|
+
|
38
|
+
def request_method_allowed?(http_request_method)
|
39
|
+
allowed_methods.include?(http_request_method)
|
40
|
+
end
|
41
|
+
|
42
|
+
def allowed_methods
|
43
|
+
Hyperdrive::Values.request_methods_symbol_map.values_at(*request_handlers.keys)
|
44
|
+
end
|
45
|
+
|
24
46
|
private
|
25
47
|
|
26
48
|
def default_allowed_params
|
@@ -42,5 +64,9 @@ module Hyperdrive
|
|
42
64
|
def default_filter_options
|
43
65
|
{ required: false }.freeze
|
44
66
|
end
|
67
|
+
|
68
|
+
def default_request_handlers
|
69
|
+
{ options: proc { '' } }
|
70
|
+
end
|
45
71
|
end
|
46
72
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
class Response
|
5
|
+
attr_reader :resource, :env, :http_request_method, :headers
|
6
|
+
|
7
|
+
def initialize(env, resource)
|
8
|
+
@resource = resource
|
9
|
+
@env = env
|
10
|
+
@http_request_method = env['REQUEST_METHOD']
|
11
|
+
|
12
|
+
unless request_method_supported?
|
13
|
+
raise Errors::NotImplemented.new(http_request_method)
|
14
|
+
end
|
15
|
+
|
16
|
+
unless resource.request_method_allowed?(http_request_method)
|
17
|
+
raise Errors::MethodNotAllowed.new(http_request_method)
|
18
|
+
end
|
19
|
+
|
20
|
+
@headers = default_headers
|
21
|
+
end
|
22
|
+
|
23
|
+
def response
|
24
|
+
@headers.merge({ 'Content-Type' => 'text/plain' })
|
25
|
+
status = (http_request_method == 'POST') ? 201 : 200
|
26
|
+
::Rack::Response.new(resource.request_handler(http_request_method).call(env), status, headers).finish
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def default_headers
|
32
|
+
{
|
33
|
+
'Allow' => resource.allowed_methods
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def request_method_supported?
|
38
|
+
Hyperdrive::Values.request_methods_string_map.key?(http_request_method)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
class Server
|
5
|
+
def self.call(env)
|
6
|
+
server.call(env)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def self.server
|
12
|
+
Rack::Builder.new do
|
13
|
+
hyperdrive.resources.each do |key, resource|
|
14
|
+
map resource.endpoint do
|
15
|
+
use Rack::Runtime
|
16
|
+
use Rack::Lint
|
17
|
+
use Rack::Head
|
18
|
+
run ->(env) {
|
19
|
+
begin
|
20
|
+
Hyperdrive::Response.new(env, resource).response
|
21
|
+
rescue Hyperdrive::Errors::HTTPError => error
|
22
|
+
[error.http_status_code, { 'Allow' => resource.allowed_methods.join(',') }, [error.message]]
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end.to_app
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Hyperdrive
|
2
|
+
module Values
|
3
|
+
def self.request_methods
|
4
|
+
%w(GET HEAD OPTIONS POST PUT PATCH DELETE).freeze
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.request_methods_symbol_map
|
8
|
+
{
|
9
|
+
get: 'GET',
|
10
|
+
head: 'HEAD',
|
11
|
+
options: 'OPTIONS',
|
12
|
+
post: 'POST',
|
13
|
+
put: 'PUT',
|
14
|
+
patch: 'PATCH',
|
15
|
+
delete: 'DELETE'
|
16
|
+
}.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.request_methods_string_map
|
20
|
+
{
|
21
|
+
'GET' => :get,
|
22
|
+
'HEAD' => :head,
|
23
|
+
'OPTIONS' => :options,
|
24
|
+
'POST' => :post,
|
25
|
+
'PUT' => :put,
|
26
|
+
'PATCH' => :patch,
|
27
|
+
'DELETE' => :delete
|
28
|
+
}.freeze
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/hyperdrive/version.rb
CHANGED
data/lib/hyperdrive.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# stdlib
|
4
|
+
require 'rack'
|
5
|
+
require 'linguistics'
|
6
|
+
Linguistics.use(:en)
|
7
|
+
|
8
|
+
# prepare for hyperspace!
|
9
|
+
require 'hyperdrive/docs'
|
10
|
+
require 'hyperdrive/dsl'
|
11
|
+
require 'hyperdrive/errors'
|
3
12
|
require 'hyperdrive/resource'
|
13
|
+
require 'hyperdrive/response'
|
14
|
+
require 'hyperdrive/server'
|
15
|
+
require 'hyperdrive/values'
|
4
16
|
require 'hyperdrive/version'
|
5
|
-
|
6
|
-
module Hyperdrive
|
7
|
-
# spinning up the FTL drive
|
8
|
-
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Hyperdrive::Docs do
|
6
|
+
before do
|
7
|
+
sample_api
|
8
|
+
@docs = Hyperdrive::Docs.new(hyperdrive.resources)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'generates a header with size 1 as default' do
|
12
|
+
@docs.header('Thing Resource').must_equal "\n\n# Thing Resource\n\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'generates a header only between size 1 and 6' do
|
16
|
+
proc {@docs.header('Thing Resource', 0)}.must_raise ArgumentError
|
17
|
+
proc {@docs.header('Thing Resource', 8)}.must_raise ArgumentError
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'generates a paragraph' do
|
21
|
+
@docs.paragraph('Description of Thing Resource').must_equal "Description of Thing Resource\n\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'generates bold text' do
|
25
|
+
@docs.bold('name').must_equal '__name__'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'generates code formatted text' do
|
29
|
+
@docs.code('/things').must_equal '`/things`'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'generates a bullet with nest level of 1 as default' do
|
33
|
+
@docs.bullet('test').must_equal " - test\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'generates a bullet with nest level between 1 and 3' do
|
37
|
+
proc {@docs.bullet('test', 4)}.must_raise ArgumentError
|
38
|
+
proc {@docs.bullet('test', 0)}.must_raise ArgumentError
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'generates a nested bulleted list' do
|
42
|
+
@docs.bullet('test', 2).must_equal " - test\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'generates a nested bullet code span' do
|
46
|
+
@docs.bullet('`/things`', 2).must_equal " - `/things`\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'generates nested bulleted bold text' do
|
50
|
+
@docs.bullet('__id__', 3).must_equal " - __id__\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'outputs a string of the completed doc' do
|
54
|
+
@docs.output.must_be_kind_of String
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Hyperdrive::DSL::Main do
|
6
|
+
it "creates a singleton instance of hyperdrive" do
|
7
|
+
hyperdrive do
|
8
|
+
end.must_be_instance_of ::Hyperdrive::DSL::Main
|
9
|
+
end
|
10
|
+
|
11
|
+
it "registers a resource" do
|
12
|
+
hyperdrive do
|
13
|
+
resource(:thing) {}
|
14
|
+
end.resources[:thing].must_be_instance_of ::Hyperdrive::Resource
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Hyperdrive::DSL::Resource do
|
6
|
+
|
7
|
+
it "sets the name of the endpoint" do
|
8
|
+
hyperdrive do
|
9
|
+
resource(:thing) do
|
10
|
+
name "Thing Resource"
|
11
|
+
end
|
12
|
+
end.resources[:thing].name.must_equal 'Thing Resource'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "describes the endpoint" do
|
16
|
+
hyperdrive do
|
17
|
+
resource(:thing) do
|
18
|
+
desc "Thing Description"
|
19
|
+
end
|
20
|
+
end.resources[:thing].desc.must_equal 'Thing Description'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "registers an allowed param on a resource" do
|
24
|
+
hyperdrive do
|
25
|
+
resource(:thing) do
|
26
|
+
param :name, "Thing's Name"
|
27
|
+
end
|
28
|
+
end.resources[:thing].allowed_params[:name][:desc].must_equal "Thing's Name"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "registers an allowed param on a resource" do
|
32
|
+
hyperdrive do
|
33
|
+
resource(:thing) do
|
34
|
+
filter :parent_id, "Thing's Parent ID"
|
35
|
+
end
|
36
|
+
end.resources[:thing].filters[:parent_id][:desc].must_equal "Thing's Parent ID"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "defines how requests are handled" do
|
40
|
+
hyperdrive do
|
41
|
+
resource(:thing) do
|
42
|
+
request(:get) do
|
43
|
+
'ok'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end.resources[:thing].request_handlers[:get].call.must_equal 'ok'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "raises an exception if request method argument is unknown" do
|
50
|
+
proc do
|
51
|
+
hyperdrive do
|
52
|
+
resource(:thing) do
|
53
|
+
request(:verb)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end.must_raise Hyperdrive::Errors::DSL::UnknownArgument
|
57
|
+
end
|
58
|
+
end
|
@@ -5,10 +5,6 @@ describe Hyperdrive::Resource do
|
|
5
5
|
@resource = Hyperdrive::Resource.new(:thing)
|
6
6
|
end
|
7
7
|
|
8
|
-
it "creates a new resource" do
|
9
|
-
@resource.resource.must_equal :thing
|
10
|
-
end
|
11
|
-
|
12
8
|
it "has a name" do
|
13
9
|
@resource.name = 'Thing'
|
14
10
|
@resource.name.must_equal 'Thing'
|
@@ -19,6 +15,10 @@ describe Hyperdrive::Resource do
|
|
19
15
|
@resource.desc.must_equal 'Description of Thing Resource'
|
20
16
|
end
|
21
17
|
|
18
|
+
it "has an endpoint" do
|
19
|
+
@resource.endpoint.must_equal '/things'
|
20
|
+
end
|
21
|
+
|
22
22
|
it "auto-registers the :id param" do
|
23
23
|
@resource.allowed_params[:id][:desc].must_equal 'Resource Identifier'
|
24
24
|
@resource.allowed_params[:id][:required].must_equal %w(PUT PATCH DELETE)
|
@@ -32,7 +32,7 @@ describe Hyperdrive::Resource do
|
|
32
32
|
|
33
33
|
it "auto-registers the :id filter" do
|
34
34
|
@resource.filters[:id][:desc].must_equal 'Resource Identifier'
|
35
|
-
@resource.filters[:id][:required].must_equal false
|
35
|
+
@resource.filters[:id][:required].must_equal false
|
36
36
|
end
|
37
37
|
|
38
38
|
it "registers a filter" do
|
@@ -40,4 +40,28 @@ describe Hyperdrive::Resource do
|
|
40
40
|
@resource.filters[:parent_id][:desc].must_equal 'Parent ID of Thing'
|
41
41
|
@resource.filters[:parent_id][:required].must_equal true
|
42
42
|
end
|
43
|
+
|
44
|
+
it "defines a request handler" do
|
45
|
+
@resource.define_request_handler(:get, Proc.new { return 'ok' })
|
46
|
+
@resource.request_handlers[:get].call.must_equal 'ok'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns the specified request handler" do
|
50
|
+
@resource.define_request_handler(:get, Proc.new { return 'ok' })
|
51
|
+
@resource.request_handler('GET').call.must_equal 'ok'
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns true if the request can be handled" do
|
55
|
+
@resource.define_request_handler(:get, Proc.new { return 'ok' })
|
56
|
+
@resource.request_method_allowed?('GET').must_equal true
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns false if the request can not be handled" do
|
60
|
+
@resource.request_method_allowed?('GET').must_equal false
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns the request methods that can handled" do
|
64
|
+
@resource.define_request_handler(:get, Proc.new { return 'ok' })
|
65
|
+
@resource.allowed_methods.must_equal ['OPTIONS','GET','HEAD']
|
66
|
+
end
|
43
67
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Hyperdrive::Response do
|
6
|
+
before do
|
7
|
+
@resource = Hyperdrive::Resource.new(:things)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "throws an error when the request method is unspported by hyperdrive" do
|
11
|
+
proc { Hyperdrive::Response.new({ 'REQUEST_METHOD' => 'TRACE' }, @resource) }.must_raise Hyperdrive::Errors::NotImplemented
|
12
|
+
end
|
13
|
+
|
14
|
+
it "throws an error when the request method is not allowed" do
|
15
|
+
proc { Hyperdrive::Response.new({ 'REQUEST_METHOD' => 'GET' }, @resource) }.must_raise Hyperdrive::Errors::MethodNotAllowed
|
16
|
+
end
|
17
|
+
|
18
|
+
it "responds to requests" do
|
19
|
+
@resource.define_request_handler(:get, Proc.new { return 'ok' })
|
20
|
+
response = Hyperdrive::Response.new({ 'REQUEST_METHOD' => 'GET' }, @resource)
|
21
|
+
response.response.must_equal 'ok'
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
describe Hyperdrive::Server do
|
4
|
+
it "handles GET requests successfully"
|
5
|
+
it "handles HEAD requests successfully"
|
6
|
+
it "handles OPTIONS requests successfully"
|
7
|
+
it "handles POST requests successfully"
|
8
|
+
it "handles PUT requests successfully"
|
9
|
+
it "handles PATCH requests successfully"
|
10
|
+
it "handles DELETE requests successfully"
|
11
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,13 +4,15 @@
|
|
4
4
|
ENV['RACK_ENV'] = 'test'
|
5
5
|
lib_path = File.expand_path('../lib', __FILE__)
|
6
6
|
($:.unshift lib_path) unless ($:.include? lib_path)
|
7
|
+
|
8
|
+
# require dependencies
|
7
9
|
require 'bundler'
|
8
10
|
Bundler.setup(:default, ENV['RACK_ENV'])
|
9
11
|
|
10
|
-
# test subject
|
12
|
+
# our humble test subject
|
11
13
|
require 'hyperdrive'
|
12
14
|
|
13
|
-
# BDD Stack
|
15
|
+
# Fire up the BDD Stack
|
14
16
|
require 'minitest/autorun'
|
15
17
|
require "minitest-spec-context"
|
16
18
|
require 'minitest/reporters'
|
@@ -18,3 +20,20 @@ require 'minitest/reporters'
|
|
18
20
|
# all systems go
|
19
21
|
MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
|
20
22
|
#include Rack::Test::Methods
|
23
|
+
|
24
|
+
def sample_api
|
25
|
+
hyperdrive do
|
26
|
+
resource(:thing) do
|
27
|
+
name 'Thing Resource'
|
28
|
+
desc 'Description of Thing Resource'
|
29
|
+
|
30
|
+
param :name, '50 Chars or less'
|
31
|
+
param :start_date, 'Format: YYYY-MM-DD', required: false
|
32
|
+
param :end_date, 'Format: YYYY-MM-DD', required: false
|
33
|
+
|
34
|
+
filter :start_date, 'Format: YYYY-MM-DD'
|
35
|
+
filter :end_date, 'Format: YYYY-MM-DD'
|
36
|
+
filter :parent_id, 'Parent ID of Thing', required: true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
CHANGED
@@ -1,34 +1,101 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hyperdrive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- StyleSeek Engineering
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-04-
|
12
|
-
dependencies:
|
13
|
-
|
11
|
+
date: 2014-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: linguistics
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: thor
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Ruby DSL for defining self-documenting, HATEOAS™ complaint, Hypermedia
|
14
56
|
API endpoints.
|
15
57
|
email:
|
16
58
|
- engineering@styleseek.com
|
17
|
-
executables:
|
59
|
+
executables:
|
60
|
+
- hyperdrive
|
18
61
|
extensions: []
|
19
62
|
extra_rdoc_files: []
|
20
63
|
files:
|
64
|
+
- ".coveralls.yml"
|
21
65
|
- ".gitignore"
|
22
66
|
- ".travis.yml"
|
23
67
|
- Gemfile
|
24
68
|
- LICENSE.txt
|
25
69
|
- README.md
|
26
70
|
- Rakefile
|
71
|
+
- bin/console
|
72
|
+
- bin/hyperdrive
|
27
73
|
- hyperdrive.gemspec
|
28
74
|
- lib/hyperdrive.rb
|
75
|
+
- lib/hyperdrive/docs.rb
|
76
|
+
- lib/hyperdrive/dsl.rb
|
77
|
+
- lib/hyperdrive/dsl/main.rb
|
78
|
+
- lib/hyperdrive/dsl/resource.rb
|
79
|
+
- lib/hyperdrive/errors.rb
|
80
|
+
- lib/hyperdrive/errors/bad_request.rb
|
81
|
+
- lib/hyperdrive/errors/dsl/unknown_argument.rb
|
82
|
+
- lib/hyperdrive/errors/http_error.rb
|
83
|
+
- lib/hyperdrive/errors/internal_server_error.rb
|
84
|
+
- lib/hyperdrive/errors/method_not_allowed.rb
|
85
|
+
- lib/hyperdrive/errors/not_found.rb
|
86
|
+
- lib/hyperdrive/errors/not_implemented.rb
|
87
|
+
- lib/hyperdrive/errors/unauthorized.rb
|
29
88
|
- lib/hyperdrive/resource.rb
|
89
|
+
- lib/hyperdrive/response.rb
|
90
|
+
- lib/hyperdrive/server.rb
|
91
|
+
- lib/hyperdrive/values.rb
|
30
92
|
- lib/hyperdrive/version.rb
|
93
|
+
- spec/hyperdrive/docs_spec.rb
|
94
|
+
- spec/hyperdrive/dsl/main_spec.rb
|
95
|
+
- spec/hyperdrive/dsl/resource_spec.rb
|
31
96
|
- spec/hyperdrive/resource_spec.rb
|
97
|
+
- spec/hyperdrive/response_spec.rb
|
98
|
+
- spec/hyperdrive/server_spec.rb
|
32
99
|
- spec/spec_helper.rb
|
33
100
|
homepage: https://github.com/styleseek/hyperdrive
|
34
101
|
licenses:
|
@@ -55,5 +122,10 @@ signing_key:
|
|
55
122
|
specification_version: 4
|
56
123
|
summary: Hypermedia State Machine
|
57
124
|
test_files:
|
125
|
+
- spec/hyperdrive/docs_spec.rb
|
126
|
+
- spec/hyperdrive/dsl/main_spec.rb
|
127
|
+
- spec/hyperdrive/dsl/resource_spec.rb
|
58
128
|
- spec/hyperdrive/resource_spec.rb
|
129
|
+
- spec/hyperdrive/response_spec.rb
|
130
|
+
- spec/hyperdrive/server_spec.rb
|
59
131
|
- spec/spec_helper.rb
|