sinatra-chiro 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.md +119 -0
- data/lib/sinatra/chiro/document.rb +25 -0
- data/lib/sinatra/chiro/endpoint.rb +39 -0
- data/lib/sinatra/chiro/monkey_patch.rb +33 -0
- data/lib/sinatra/chiro/parameters/array.rb +14 -0
- data/lib/sinatra/chiro/parameters/base.rb +44 -0
- data/lib/sinatra/chiro/parameters/boolean.rb +18 -0
- data/lib/sinatra/chiro/parameters/date.rb +22 -0
- data/lib/sinatra/chiro/parameters/datetime.rb +21 -0
- data/lib/sinatra/chiro/parameters/fixnum.rb +11 -0
- data/lib/sinatra/chiro/parameters/float.rb +11 -0
- data/lib/sinatra/chiro/parameters/parameter_factory.rb +26 -0
- data/lib/sinatra/chiro/parameters/regexp.rb +22 -0
- data/lib/sinatra/chiro/parameters/string.rb +16 -0
- data/lib/sinatra/chiro/parameters/time.rb +22 -0
- data/lib/sinatra/chiro/validate.rb +47 -0
- data/lib/sinatra/chiro.rb +118 -0
- data/lib/sinatra/chiro_version.rb +5 -0
- data/lib/views/help.erb +259 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NzYwZmYwNTJjMDNmMzhkMDAxNzVmYzkxYTE0MTM4ZjYyMDcxZDFjZQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MDdiNzJhMzQyMDAxNTkyZmY1MzUzNDJmZWI3ZmE0YjFkN2I2MGJlOA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDFlNDliMTIzOGE3MjYxMTQzYjRjYzZiODY5NzZmZGVlNTY1YjJmMTg3YzU5
|
10
|
+
YmQ2ZTVmZGI1MTczYjhmYTc5OTlkOGU4ZGRhZDA1MWFjMjE0ZWY0M2I3MjI0
|
11
|
+
MDE2YjA5MDZlYmE2YTAwMTRjZDI1OTc1NGQ5OTQ4ZmU5MWViNjA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTE4OTI4YmQ2ZmZiYzY3MDczZTBhNDM0NjI0ODUxOTgxMzI0NDAwNmM5ZmMx
|
14
|
+
ZGQ2YjVkOWIzNzY3Mjg1MGI1MWI1NGUyNTk1MDIxZWExMDQ1ZmNmZTUyNWNm
|
15
|
+
OGEwYTQyM2MwZWU4YjYzMzZiYTdkZmRjYmU4NzgyY2MwZmMwNDk=
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
Copyright (c) 2013 Richard Nienaber
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
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 NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
19
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
20
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
chiro
|
2
|
+
=============
|
3
|
+
|
4
|
+
Chiro is a DSL for your sinatra application which generates readable documentation and validates the parameters used in your API.
|
5
|
+
|
6
|
+
The generated documentation includes details of all required parameters for a given route, and can optionally include an example response and possible errors which may occur.
|
7
|
+
|
8
|
+
This information can then be viewed for a specific route by including "help" as a query string parameter in the relevent URL, or the documentation for all routes can be viewed by using the path /routes.
|
9
|
+
|
10
|
+
|
11
|
+
## Implementation
|
12
|
+
|
13
|
+
In your server file, every request must be contained within an `endpoint` block. This block must also contain any parameters used by the request in order for them to be documented and validated. Below is a simple example demonstrating the declaration of an endpoint.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
app_description "Greeter Application"
|
17
|
+
|
18
|
+
endpoint 'Greeter', 'greets the user by name' do
|
19
|
+
named_param(:name, 'name of the user', :type => String)
|
20
|
+
query_param(:greeting, 'how the user wants to be greeted', :type => String, :optional => false)
|
21
|
+
get '/greet/:name' do
|
22
|
+
"#{params[:greeting]}, #{params[:name]}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
The application must first be named using `app_description`, as shown above, then any endpoints must be declared in the format:
|
28
|
+
|
29
|
+
endpoint 'Name of feature', 'description of feature'
|
30
|
+
|
31
|
+
|
32
|
+
## Declaring Parameters
|
33
|
+
|
34
|
+
If a parameter received via a request has not been declared, or has been declared improperly, a validation error will arise. There are three types of parameters which can be declared in the endpoint:
|
35
|
+
|
36
|
+
Query Parameters are found within the query string (eg. /greeter?greeting=hello)
|
37
|
+
|
38
|
+
query_param(:greeting, 'how the user wants to be greeted', :type => String, :optional => true)
|
39
|
+
|
40
|
+
Named Parameters are found between forward slashes in the path (eg. /person/:height)
|
41
|
+
|
42
|
+
named_param(:height, 'the height of the person', :type => Float)
|
43
|
+
|
44
|
+
Form parameters, which are obtained using a post request.
|
45
|
+
|
46
|
+
form(:birthday, 'the date of birth of the user', :type => Date, :optional => false)
|
47
|
+
|
48
|
+
The first two arguments should be the name of the parameter, and a brief description.
|
49
|
+
|
50
|
+
The `:type` argument can be one of `String`, `Fixnum`, `Float`, `Date`, `Time`, `DateTime`, `:boolean`, `Array` or the regular expression for matching. If a value for `:type` is not given, it will default to `String` and will be validated as such.
|
51
|
+
|
52
|
+
If `:optional` is false, a validation error will be raised when the parameter has not been given. The value of `:optional` defaults to false for named parameters, and true for query and form parameters.
|
53
|
+
|
54
|
+
If your application assigns a default value to parameters, the optional argument `:default` can be used to tell this to Chiro. For example:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
query_param(:greeting, 'how the user wants to be greeted', :type => String, :optional => true, :default => 'hello')
|
58
|
+
get '/greet/:name' do
|
59
|
+
greeting = params[:greeting] || 'Hello'
|
60
|
+
"#{greeting}"
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
|
65
|
+
## Regular Expressions
|
66
|
+
|
67
|
+
To customise the validation process to validate a parameter according to a regular expression, you substitute the regular expression as the value of the `:type` argument. It is also useful to add to the declaration a `:comment` argument to explain the expression, and `:type_description`, which updates the type in the documentation.
|
68
|
+
|
69
|
+
For example, you might want a `:gender` parameter which only accepts "male" or "female", and would want it to be validated accordingly.
|
70
|
+
|
71
|
+
```
|
72
|
+
query_param(:gender, 'gender parameter of regexp type', :type => /^male$|^female$/, :type_description => "male|female", :comment => 'Must be "male" or "female"')
|
73
|
+
```
|
74
|
+
|
75
|
+
|
76
|
+
## Possible Errors
|
77
|
+
|
78
|
+
It is also possible for your generated documentation to include details of possible errors which may occur. These can be declared within the endpoint in a similar way to parameters, but with a description and a status code. For example:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
possible_error('invalid_request_error', 400, 'Invalid request errors arise when your request has invalid parameters')
|
82
|
+
```
|
83
|
+
|
84
|
+
|
85
|
+
## Responses
|
86
|
+
|
87
|
+
Example responses can also be included this way by including `response` within the endpoint with a hash of all given parameters as the argument.
|
88
|
+
|
89
|
+
```
|
90
|
+
response({:string => 'Richard', :date => '1981-01-01', :time => '12:00:00', :fixnum => 24, :float => 1.2, :array => [1,2,3,4,5]})
|
91
|
+
```
|
92
|
+
|
93
|
+
This would return the following example response to the documentation:
|
94
|
+
|
95
|
+
```
|
96
|
+
{
|
97
|
+
string: Richard
|
98
|
+
date: 1981-01-01
|
99
|
+
time: 12:00:00
|
100
|
+
fixnum: 24
|
101
|
+
float: 1.2
|
102
|
+
array: [1, 2, 3, 4, 5]
|
103
|
+
}
|
104
|
+
|
105
|
+
```
|
106
|
+
|
107
|
+
|
108
|
+
## Validation
|
109
|
+
|
110
|
+
If any validation errors occur when your application is run, a response status code 403 will be returned along with a JSON object containing all errors, for example:
|
111
|
+
|
112
|
+
```json
|
113
|
+
{
|
114
|
+
"validation_errors":[
|
115
|
+
"name parameter must be a string of only letters",
|
116
|
+
"date parameter must be a string in the format: yyyy-mm-dd"
|
117
|
+
]
|
118
|
+
}
|
119
|
+
```
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
class Documentation
|
4
|
+
|
5
|
+
attr_reader :endpoints
|
6
|
+
|
7
|
+
def initialize(endpoints)
|
8
|
+
@endpoints = endpoints
|
9
|
+
end
|
10
|
+
|
11
|
+
def document(env)
|
12
|
+
_, path = env['sinatra.route'].split
|
13
|
+
endpoint = endpoints.select { |d| d.path == path}.flatten.first
|
14
|
+
raise "Path #{path} doesn't have any docs" unless endpoint
|
15
|
+
[[endpoints[0].appname, [endpoint]]]
|
16
|
+
end
|
17
|
+
|
18
|
+
def routes
|
19
|
+
[endpoints[0].appname, endpoints]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Endpoint
|
2
|
+
attr_reader :appname, :description, :verb, :path, :named_params, :query_params, :forms, :possible_errors, :response, :title
|
3
|
+
|
4
|
+
def initialize(opts)
|
5
|
+
@appname = opts[:appname]
|
6
|
+
@description = opts[:description]
|
7
|
+
@title = opts[:title]
|
8
|
+
@verb = opts[:verb]
|
9
|
+
@path = opts[:path]
|
10
|
+
@named_params = opts[:named_params]
|
11
|
+
@query_params = opts[:query_params]
|
12
|
+
@perform_validation = opts[:perform_validation]
|
13
|
+
@response = opts[:response]
|
14
|
+
@forms = opts[:forms]
|
15
|
+
@possible_errors = opts[:possible_errors]
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def route
|
20
|
+
"#{verb}: #{path}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate?
|
24
|
+
@perform_validation
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_json(*a)
|
28
|
+
{:title => title,
|
29
|
+
:description => description,
|
30
|
+
:verb => verb,
|
31
|
+
:path => path,
|
32
|
+
:named_params => named_params,
|
33
|
+
:query_params => query_params,
|
34
|
+
:forms => forms,
|
35
|
+
:possible_errors => possible_errors,
|
36
|
+
:response => response,
|
37
|
+
}.to_json
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Sinatra
|
2
|
+
class Base
|
3
|
+
#we monkey patch here because at this point we know the name of the route
|
4
|
+
alias_method :old_route_eval, :route_eval
|
5
|
+
def route_eval
|
6
|
+
show_help if params.has_key? "help"
|
7
|
+
validate_parameters if self.class.respond_to?(:validator)
|
8
|
+
|
9
|
+
old_route_eval { yield }
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate_parameters
|
13
|
+
error = self.class.validator.validate(params, env)
|
14
|
+
if error == "not found"
|
15
|
+
status 404
|
16
|
+
throw :halt, "Path not found"
|
17
|
+
elsif error!= nil
|
18
|
+
status 403
|
19
|
+
throw :halt, "#{error}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def show_help
|
24
|
+
if self.class.respond_to?(:validator)
|
25
|
+
status 200
|
26
|
+
erb_file = settings.erb_file
|
27
|
+
views = settings.views_location
|
28
|
+
throw :halt, "#{erb(erb_file, {:views => views}, :endpoint => self.class.documentation.document(env)) }"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
module Chiro
|
5
|
+
module Parameters
|
6
|
+
class ArrayValidator < Base
|
7
|
+
def validate(given)
|
8
|
+
"#{name} parameter must be an Array of Strings" if !given[name].is_a? Array
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class Base
|
5
|
+
attr_reader :options
|
6
|
+
def initialize(opts={})
|
7
|
+
@options = opts
|
8
|
+
end
|
9
|
+
|
10
|
+
def name
|
11
|
+
@options[:name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def name_display
|
15
|
+
@options[:name].to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
@options[:description]
|
20
|
+
end
|
21
|
+
|
22
|
+
def comment
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def type_description
|
27
|
+
@options[:type]
|
28
|
+
end
|
29
|
+
|
30
|
+
def type
|
31
|
+
@options[:type]
|
32
|
+
end
|
33
|
+
|
34
|
+
def default
|
35
|
+
@options[:default]
|
36
|
+
end
|
37
|
+
|
38
|
+
def optional
|
39
|
+
@options[:optional]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class BooleanValidator < Base
|
5
|
+
def validate(given)
|
6
|
+
if given[name] != "true" && given[name] != "false"
|
7
|
+
"#{name_display} parameter must be true or false"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def comment
|
12
|
+
"Must be true or false"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class DateValidator < Base
|
5
|
+
def validate(given)
|
6
|
+
Date.parse(given.to_s)
|
7
|
+
|
8
|
+
if given[name] !~ /^\d{4}-\d{2}-\d{2}$/
|
9
|
+
"#{name_display} parameter must be a string in the format: yyyy-mm-dd"
|
10
|
+
end
|
11
|
+
rescue ArgumentError
|
12
|
+
"#{name_display} parameter invalid"
|
13
|
+
end
|
14
|
+
|
15
|
+
def comment
|
16
|
+
'Must be expressed according to ISO 8601 (ie. YYYY-MM-DD)'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class DateTimeValidator < Base
|
5
|
+
def validate(given)
|
6
|
+
DateTime.parse(given.to_s)
|
7
|
+
|
8
|
+
if given[name] !~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/
|
9
|
+
"#{name_display} parameter must be a string in the format: yyyy-mm-ddThh:mm:ss"
|
10
|
+
end
|
11
|
+
rescue ArgumentError
|
12
|
+
"#{name_display} parameter invalid"
|
13
|
+
end
|
14
|
+
def comment
|
15
|
+
'Must be expressed according to ISO 8601 (ie. YYYY-MM-DDThh:mm:ss)'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class FloatValidator < Base
|
5
|
+
def validate(given)
|
6
|
+
"#{name_display} parameter must be a Float" if given[name]!~/^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class ParameterFactory
|
5
|
+
def self.validator_from_type(options)
|
6
|
+
validator = case options[:type].to_s
|
7
|
+
when 'String' then StringValidator
|
8
|
+
when 'Fixnum' then FixnumValidator
|
9
|
+
when 'Float' then FloatValidator
|
10
|
+
when 'Date' then DateValidator
|
11
|
+
when 'DateTime' then DateTimeValidator
|
12
|
+
when 'Time' then TimeValidator
|
13
|
+
when 'boolean' then BooleanValidator
|
14
|
+
when '[String]' then ArrayValidator
|
15
|
+
else
|
16
|
+
if options[:type].is_a? Regexp
|
17
|
+
RegexpValidator
|
18
|
+
end
|
19
|
+
end
|
20
|
+
validator.new(options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class RegexpValidator < Base
|
5
|
+
|
6
|
+
def validate(given)
|
7
|
+
"#{name_display} parameter should match regexp: #{options[:type]}" if given[name] !~ options[:type]
|
8
|
+
end
|
9
|
+
|
10
|
+
def type_description
|
11
|
+
super || "Regexp"
|
12
|
+
end
|
13
|
+
|
14
|
+
def comment
|
15
|
+
"#{options[:comment]}"
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class StringValidator < Base
|
5
|
+
def validate(given)
|
6
|
+
if given[name_display] !~/^[a-zA-Z]*$/
|
7
|
+
"#{name_display} parameter must be a string of only letters"
|
8
|
+
elsif given[name_display].empty?
|
9
|
+
"#{name_display} parameter must not be empty"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Chiro
|
3
|
+
module Parameters
|
4
|
+
class TimeValidator < Base
|
5
|
+
def validate(given)
|
6
|
+
Time.parse(given.to_s)
|
7
|
+
|
8
|
+
if given[name] !~ /^\d{2}:\d{2}:\d{2}$/
|
9
|
+
"#{name_display} parameter must be a string in the format: hh:mm:ss"
|
10
|
+
end
|
11
|
+
rescue ArgumentError
|
12
|
+
"#{name_display} parameter invalid"
|
13
|
+
end
|
14
|
+
|
15
|
+
def comment
|
16
|
+
'Must be expressed according to ISO 8601 (ie. hh:mm:ss)'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
module Chiro
|
5
|
+
class Validation
|
6
|
+
attr_reader :endpoints
|
7
|
+
|
8
|
+
def initialize(endpoints)
|
9
|
+
@endpoints = endpoints
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(params, env)
|
13
|
+
_, path = env['sinatra.route'].split
|
14
|
+
|
15
|
+
endpoint = endpoints.select { |d| d.path == path }.flatten.first
|
16
|
+
return if endpoint.nil?
|
17
|
+
|
18
|
+
all_given = params.dup
|
19
|
+
all_given.delete('captures')
|
20
|
+
all_given.delete('splat')
|
21
|
+
|
22
|
+
all_params = endpoint.named_params + endpoint.query_params + endpoint.forms
|
23
|
+
|
24
|
+
allowed_params = []
|
25
|
+
errors = []
|
26
|
+
|
27
|
+
all_params.each do |parameter|
|
28
|
+
unless all_given[parameter.name_display].nil?
|
29
|
+
errors << parameter.validate(all_given)
|
30
|
+
end
|
31
|
+
if !parameter.optional
|
32
|
+
errors << "must include a #{parameter.name_display} parameter" if all_given[parameter.name_display].nil?
|
33
|
+
end
|
34
|
+
allowed_params << parameter.name_display
|
35
|
+
end
|
36
|
+
|
37
|
+
all_given.map { |k, _| k.to_s}.each do |param|
|
38
|
+
errors << "#{param} is not a valid parameter" if !allowed_params.include?(param)
|
39
|
+
end
|
40
|
+
|
41
|
+
if !errors.compact.empty? then
|
42
|
+
JSON.dump ({:validation_errors => errors.compact})
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'sinatra/chiro/endpoint'
|
2
|
+
require 'sinatra/chiro/document'
|
3
|
+
require 'sinatra/chiro/validate'
|
4
|
+
require 'sinatra/chiro/monkey_patch'
|
5
|
+
Dir[File.dirname(__FILE__) + '/chiro/parameters/*.rb'].sort.each { |f| require f}
|
6
|
+
|
7
|
+
CHIRO_APPS = []
|
8
|
+
module Sinatra
|
9
|
+
module Chiro
|
10
|
+
def endpoints
|
11
|
+
@endpoints ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def validator
|
15
|
+
@validator ||= Validation.new(endpoints)
|
16
|
+
end
|
17
|
+
|
18
|
+
def documentation
|
19
|
+
@documentation ||= Documentation.new(endpoints)
|
20
|
+
end
|
21
|
+
|
22
|
+
def app_description(description)
|
23
|
+
@app_description = description
|
24
|
+
end
|
25
|
+
|
26
|
+
def endpoint(title=nil, description=nil, opts={})
|
27
|
+
opts[:title] ||= title
|
28
|
+
opts[:description] ||= description
|
29
|
+
opts[:perform_validation] ||= true
|
30
|
+
@named_params = []
|
31
|
+
@query_params = []
|
32
|
+
@forms = []
|
33
|
+
@possible_errors = []
|
34
|
+
@response = nil
|
35
|
+
@appname = @app_description || self.name
|
36
|
+
|
37
|
+
yield
|
38
|
+
|
39
|
+
opts[:verb] = @verb || :GET
|
40
|
+
opts[:named_params] = @named_params
|
41
|
+
opts[:query_params] = @query_params
|
42
|
+
opts[:forms] = @forms
|
43
|
+
opts[:possible_errors] = @possible_errors
|
44
|
+
opts[:response] = @response
|
45
|
+
opts[:path] = @path
|
46
|
+
opts[:appname] = @appname
|
47
|
+
endpoints << Endpoint.new(opts)
|
48
|
+
end
|
49
|
+
|
50
|
+
def named_param(name, description, opts={})
|
51
|
+
opts.merge!(:name => name, :description => description, :optional => false)
|
52
|
+
Chiro.remove_unknown_param_keys(opts)
|
53
|
+
Chiro.set_param_defaults(opts)
|
54
|
+
@named_params << Parameters::ParameterFactory.validator_from_type(opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
def form(name, description, opts={})
|
58
|
+
opts.merge!(:name => name, :description => description)
|
59
|
+
Chiro.remove_unknown_param_keys(opts)
|
60
|
+
Chiro.set_param_defaults(opts)
|
61
|
+
@forms << Parameters::ParameterFactory.validator_from_type(opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
def query_param(name, description, opts={})
|
65
|
+
opts.merge!(:name => name, :description => description)
|
66
|
+
Chiro.set_param_defaults(opts)
|
67
|
+
@query_params << Parameters::ParameterFactory.validator_from_type(opts)
|
68
|
+
end
|
69
|
+
|
70
|
+
def possible_error(name, code, description)
|
71
|
+
@possible_errors << {:name => name, :code => code, :description => description}
|
72
|
+
end
|
73
|
+
|
74
|
+
def get(path, opts = {}, &block)
|
75
|
+
@path = path
|
76
|
+
@verb = :GET
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
def post(path, opts = {}, &block)
|
81
|
+
@path = path
|
82
|
+
@verb = :POST
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def response(result)
|
87
|
+
@response = result
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.registered(app)
|
91
|
+
CHIRO_APPS << app
|
92
|
+
|
93
|
+
app.set :erb_file, :help
|
94
|
+
app.set :views_location, File.join(File.dirname(__FILE__), '..', 'views')
|
95
|
+
|
96
|
+
app.get '/routes' do
|
97
|
+
routes = CHIRO_APPS.select { |a| a.respond_to?(:documentation) }.map { |a| a.documentation.routes }
|
98
|
+
erb_file = settings.erb_file
|
99
|
+
views = settings.views_location
|
100
|
+
erb(erb_file, {:views => views}, :endpoint => routes)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
def self.remove_unknown_param_keys(opts)
|
106
|
+
known_options = [:name, :description, :default, :type, :optional, :validator, :comment, :type_description]
|
107
|
+
opts.delete_if { |k| !k.is_a?(Symbol) || !known_options.include?(k)}
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.set_param_defaults(opts)
|
111
|
+
opts[:type] ||= String
|
112
|
+
opts[:optional] ||= true if opts[:optional].nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
register Sinatra::Chiro
|
118
|
+
end
|
data/lib/views/help.erb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
<!DOCTYPE html>
|
4
|
+
<!--[if lt IE 7]>
|
5
|
+
<html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
6
|
+
<!--[if IE 7]>
|
7
|
+
<html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
8
|
+
<!--[if IE 8]>
|
9
|
+
<html class="no-js lt-ie9"> <![endif]-->
|
10
|
+
<!--[if gt IE 8]><!-->
|
11
|
+
<html class="no-js"> <!--<![endif]-->
|
12
|
+
<head>
|
13
|
+
<meta charset="utf-8">
|
14
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
15
|
+
<title>Documentation</title>
|
16
|
+
<meta name="description" content="">
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
18
|
+
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0-rc1/css/bootstrap.min.css" rel="stylesheet">
|
19
|
+
<style>
|
20
|
+
.scrollspy-example {
|
21
|
+
overflow: auto;
|
22
|
+
position: relative;
|
23
|
+
height: auto;
|
24
|
+
}
|
25
|
+
|
26
|
+
thead {
|
27
|
+
background-color: #D3EDFB;
|
28
|
+
}
|
29
|
+
|
30
|
+
.container {
|
31
|
+
padding-top: 0;
|
32
|
+
margin-left: 15px;
|
33
|
+
height: 100%
|
34
|
+
}
|
35
|
+
|
36
|
+
body {
|
37
|
+
color: #54606c;
|
38
|
+
}
|
39
|
+
|
40
|
+
.hero-unit {
|
41
|
+
padding: 30px;
|
42
|
+
border-bottom: solid thin #DDDDDD;
|
43
|
+
}
|
44
|
+
|
45
|
+
p {
|
46
|
+
color: #73C3ED;
|
47
|
+
font-size: 16px;
|
48
|
+
padding-left: 15px;
|
49
|
+
}
|
50
|
+
|
51
|
+
h1 {
|
52
|
+
padding-top: 30px;
|
53
|
+
}
|
54
|
+
|
55
|
+
h2 {
|
56
|
+
padding-left: 15px;
|
57
|
+
}
|
58
|
+
|
59
|
+
.table {
|
60
|
+
font-size: 14px;
|
61
|
+
}
|
62
|
+
|
63
|
+
pre {
|
64
|
+
height: auto;
|
65
|
+
max-height: 200px;
|
66
|
+
line-height: 10px;
|
67
|
+
overflow: auto;
|
68
|
+
background-color: #F9F9F9
|
69
|
+
}
|
70
|
+
|
71
|
+
.sidebar {
|
72
|
+
border-left: solid thin #DDDDDD;
|
73
|
+
padding: 50px 0 0 0;
|
74
|
+
}
|
75
|
+
|
76
|
+
.nav-pills {
|
77
|
+
overflow: auto;
|
78
|
+
height: 100%;
|
79
|
+
}
|
80
|
+
|
81
|
+
li {
|
82
|
+
color: #54606c;
|
83
|
+
padding: 0 20px 0 20px;
|
84
|
+
width: 100%;
|
85
|
+
text-align: left
|
86
|
+
}
|
87
|
+
|
88
|
+
a {
|
89
|
+
color: #54606c;
|
90
|
+
}
|
91
|
+
|
92
|
+
.nav-pills>li>a:hover {
|
93
|
+
color: white;
|
94
|
+
background-color: #D3EDFB;
|
95
|
+
}
|
96
|
+
|
97
|
+
.nav-pills>li.active>a:hover {
|
98
|
+
color: #54606c;
|
99
|
+
background-color: #D3EDFB;
|
100
|
+
}
|
101
|
+
|
102
|
+
.nav-pills>li.active>a {
|
103
|
+
color: #54606c;
|
104
|
+
background-color: #D3EDFB;
|
105
|
+
}
|
106
|
+
</style>
|
107
|
+
</head>
|
108
|
+
|
109
|
+
|
110
|
+
<body data-spy="scroll" data-target="#navbarExample" data-offset="50" class="scrollspy-example">
|
111
|
+
|
112
|
+
<div class="container">
|
113
|
+
<div class="col-lg-9">
|
114
|
+
<% endpoint.each do |endpoints| %>
|
115
|
+
<h1 id="<%= endpoints[0].downcase.tr(" ", "_") %>"><%= endpoints[0] %></h1>
|
116
|
+
<% endpoints[1].each do |endpoint| %>
|
117
|
+
<div class="hero-unit" id="<%= endpoint.title.downcase.tr(" ", "_") %>">
|
118
|
+
|
119
|
+
<h2><%= endpoint.title %></h2>
|
120
|
+
<p><%= endpoint.description %></p>
|
121
|
+
<h3><%= endpoint.route %></h3>
|
122
|
+
|
123
|
+
<% if !endpoint.query_params.empty? %>
|
124
|
+
<h4>Query String Parameters:</h4>
|
125
|
+
<table class='table table-striped table-bordered table-condensed'>
|
126
|
+
<thead>
|
127
|
+
<tr>
|
128
|
+
<th>Name</th>
|
129
|
+
<th>Description</th>
|
130
|
+
<th>Type</th>
|
131
|
+
<th>Optional</th>
|
132
|
+
<th>Comment</th>
|
133
|
+
</tr>
|
134
|
+
</thead>
|
135
|
+
<% endpoint.query_params.each do |p| %>
|
136
|
+
<tbody>
|
137
|
+
<td><%= p.name %></td>
|
138
|
+
<td><%= p.description %></td>
|
139
|
+
<td><%= p.type_description %></td>
|
140
|
+
<td><%= p.optional %></td>
|
141
|
+
<td><%= p.comment %></td>
|
142
|
+
</tbody>
|
143
|
+
<% end %>
|
144
|
+
</table>
|
145
|
+
<% end %>
|
146
|
+
|
147
|
+
<% if !endpoint.named_params.empty? %>
|
148
|
+
<h4>Named Parameters:</h4>
|
149
|
+
<table class='table table-striped table-bordered table-condensed'>
|
150
|
+
<thead>
|
151
|
+
<tr>
|
152
|
+
<th>Name</th>
|
153
|
+
<th>Description</th>
|
154
|
+
<th>Type</th>
|
155
|
+
<th>Optional</th>
|
156
|
+
<th>Comment</th>
|
157
|
+
</tr>
|
158
|
+
</thead>
|
159
|
+
<% endpoint.named_params.each do |p| %>
|
160
|
+
<tbody>
|
161
|
+
<td><%= p.name %></td>
|
162
|
+
<td><%= p.description %></td>
|
163
|
+
<td><%= p.type_description %></td>
|
164
|
+
<td><%= p.optional %></td>
|
165
|
+
<td><%= p.comment %></td>
|
166
|
+
</tbody>
|
167
|
+
<% end %>
|
168
|
+
</table>
|
169
|
+
<% end %>
|
170
|
+
|
171
|
+
<% if !endpoint.forms.empty? %>
|
172
|
+
<h4>Form Parameters:</h4>
|
173
|
+
<table class='table table-striped table-bordered table-condensed'>
|
174
|
+
<thead>
|
175
|
+
<tr>
|
176
|
+
<th>Name</th>
|
177
|
+
<th>Description</th>
|
178
|
+
<th>Type</th>
|
179
|
+
<th>Optional</th>
|
180
|
+
<th>Comment</th>
|
181
|
+
</tr>
|
182
|
+
</thead>
|
183
|
+
<% endpoint.forms.each do |p| %>
|
184
|
+
<tbody>
|
185
|
+
<td><%= p.name %></td>
|
186
|
+
<td><%= p.description %></td>
|
187
|
+
<td><%= p.type_description %></td>
|
188
|
+
<td><%= p.optional %></td>
|
189
|
+
<td><%= p.comment %></td>
|
190
|
+
</tbody>
|
191
|
+
<% end %>
|
192
|
+
</table>
|
193
|
+
<% end %>
|
194
|
+
|
195
|
+
<% if !endpoint.possible_errors.empty? %>
|
196
|
+
<h4>Possible Errors:</h4>
|
197
|
+
<table class='table table-striped table-bordered table-condensed'>
|
198
|
+
<thead>
|
199
|
+
<tr>
|
200
|
+
<th>Name</th>
|
201
|
+
<th>Code</th>
|
202
|
+
<th>Description</th>
|
203
|
+
</tr>
|
204
|
+
</thead>
|
205
|
+
<% endpoint.possible_errors.each do |p| %>
|
206
|
+
<tbody>
|
207
|
+
<td><%= p[:name] %></td>
|
208
|
+
<td><%= p[:code] %></td>
|
209
|
+
<td><%= p[:description] %></td>
|
210
|
+
</tbody>
|
211
|
+
<% end %>
|
212
|
+
</table>
|
213
|
+
<% end %>
|
214
|
+
|
215
|
+
<% if !endpoint.response.nil? %>
|
216
|
+
<h4>Response:</h4>
|
217
|
+
<pre>
|
218
|
+
{
|
219
|
+
<% endpoint.response.each do |k, v| %>
|
220
|
+
<%= "#{k}: #{v}" %>
|
221
|
+
<% end %>
|
222
|
+
}
|
223
|
+
</pre>
|
224
|
+
<% end %>
|
225
|
+
</div>
|
226
|
+
<% end %>
|
227
|
+
<% end %>
|
228
|
+
</div>
|
229
|
+
|
230
|
+
<div class="row">
|
231
|
+
<div class="col-lg-3">
|
232
|
+
<div id="navbarExample">
|
233
|
+
<div class="col-lg-3">
|
234
|
+
<ul type=none class="nav nav-pills sidebar affix">
|
235
|
+
<% endpoint.each do |endpoints| %>
|
236
|
+
<li class="">
|
237
|
+
<a href='#<%= endpoints[0].downcase.tr(" ", "_")%>'><%=endpoints[0]%></a>
|
238
|
+
<ul class = "nav nav-pills">
|
239
|
+
<% endpoints[1].each do |endpoint| %>
|
240
|
+
<li class="">
|
241
|
+
<a href='#<%= endpoint.title.downcase.tr(" ", "_") %>'><%= endpoint.title %></a>
|
242
|
+
</li>
|
243
|
+
<% end %>
|
244
|
+
</ul>
|
245
|
+
</li>
|
246
|
+
<% end %>
|
247
|
+
</ul>
|
248
|
+
</div>
|
249
|
+
</div>
|
250
|
+
</div>
|
251
|
+
</div>
|
252
|
+
</div>
|
253
|
+
|
254
|
+
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
255
|
+
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0-rc1/js/bootstrap.min.js"></script>
|
256
|
+
</body>
|
257
|
+
</html>
|
258
|
+
|
259
|
+
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-chiro
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richard Nienaber
|
8
|
+
- Scott Adams
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sinatra
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ~>
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 1.4.3
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 1.4.3
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: json
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.8.0
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.8.0
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: sinatra-contrib
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ~>
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 1.4.0
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.4.0
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.13.0
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.13.0
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
description: Documents and validates sinatra api requests
|
85
|
+
email:
|
86
|
+
- rjnienaber@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE
|
93
|
+
- README.md
|
94
|
+
- lib/sinatra/chiro.rb
|
95
|
+
- lib/sinatra/chiro/document.rb
|
96
|
+
- lib/sinatra/chiro/endpoint.rb
|
97
|
+
- lib/sinatra/chiro/monkey_patch.rb
|
98
|
+
- lib/sinatra/chiro/parameters/array.rb
|
99
|
+
- lib/sinatra/chiro/parameters/base.rb
|
100
|
+
- lib/sinatra/chiro/parameters/boolean.rb
|
101
|
+
- lib/sinatra/chiro/parameters/date.rb
|
102
|
+
- lib/sinatra/chiro/parameters/datetime.rb
|
103
|
+
- lib/sinatra/chiro/parameters/fixnum.rb
|
104
|
+
- lib/sinatra/chiro/parameters/float.rb
|
105
|
+
- lib/sinatra/chiro/parameters/parameter_factory.rb
|
106
|
+
- lib/sinatra/chiro/parameters/regexp.rb
|
107
|
+
- lib/sinatra/chiro/parameters/string.rb
|
108
|
+
- lib/sinatra/chiro/parameters/time.rb
|
109
|
+
- lib/sinatra/chiro/validate.rb
|
110
|
+
- lib/sinatra/chiro_version.rb
|
111
|
+
- lib/views/help.erb
|
112
|
+
homepage: https://github.com/rjnienaber/chiro
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 2.0.6
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: An easy way to produce self-documenting sinatra apis
|
136
|
+
test_files: []
|