sinatra-swagger-exposer 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b55ef98d692d8d3d3b6d27a92b7a30657d5729bb
4
- data.tar.gz: a34f58471cf590358e549b51ac348829b269b1ae
3
+ metadata.gz: fa97395699d5708bb599a1793df03d11ee73cc6a
4
+ data.tar.gz: 8dcad9fd2667784b9a6ba79362e6109b344779d8
5
5
  SHA512:
6
- metadata.gz: fe4a2405064efbde281a34810dc78680a3a4ba484cc281f3045ef12211cf8470baadb171db8bb6fdfd53faa11bbb178a3cd3d60a3deb21283bcac17cff3af910
7
- data.tar.gz: 86f6a2d376446ed4a59e5d1049955f1cacc2eb0334895d116ab42fcdd891618863a30b5264ec81983d7ff3c453ad4e446dc9de4bb3c5bdb8cef4773c39d7eaca
6
+ metadata.gz: ea832406300901d1975504d66725fec8669f465756c75abf68fdf8e3a58af5a3a530659acb50887c2ebceccd0b9287daf89cfc4e769a3365659b05bd53d16a76
7
+ data.tar.gz: 995af3f7a7e77b2ba82ef9644856d324b9f96cc8acf582f8fdd448f3f69ebcee34a44075ae1c84fd7a066bce44ea2af4fdf59fe681554a129d88e0017fbd6834
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # 0.1.0
2
+
3
+ - Added verification and enrichment form params
4
+ - Added type extends
5
+ - Added validation for min and max value
6
+
7
+ # 0.0.1
8
+
9
+ - First version
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Sinatra::SwaggerExposer
2
+
3
+ [![Code Climate](https://codeclimate.com/github/archiloque/sinatra-swagger-exposer/badges/gpa.svg)](https://codeclimate.com/github/archiloque/sinatra-swagger-exposer)
4
+ [![Build Status](https://travis-ci.org/archiloque/sinatra-swagger-exposer.svg?branch=master)](https://travis-ci.org/archiloque/sinatra-swagger-exposer)
5
+ [![Coverage Status](https://coveralls.io/repos/archiloque/sinatra-swagger-exposer/badge.svg?branch=master)](https://coveralls.io/r/archiloque/sinatra-swagger-exposer?branch=master)
6
+
7
+ Create Swagger endpoint for your Sinatra application.
8
+
9
+ This Sinatra extension enable you to add metadata to your code to
10
+
11
+ - expose your API as a [Swagger](http://swagger.io) endpoint.
12
+ - validate and enrich the invocation parameters
13
+
14
+ I'm adding features as I need them and it currently doesn't use all the Swagger options, so if you need one that is missing please open an issue.
15
+
16
+ ## Design choices
17
+
18
+ - All the declarations are validated when the server is started
19
+ - The declarations are defined to look as ruby-ish as possible
20
+ - Declarations are used for parameters validation and enrichment
21
+
22
+ ## Usage
23
+
24
+ To use it in your app :
25
+
26
+ ```ruby
27
+ require 'sinatra/swagger-exposer/swagger-exposer'
28
+
29
+ class MyApp < Sinatra::Base
30
+
31
+ register Sinatra::SwaggerExposer
32
+
33
+ general_info(
34
+ {
35
+ version: '0.0.1',
36
+ title: 'My app',
37
+ description: 'My wonderful app',
38
+ license: {
39
+ name: 'MIT',
40
+ url: 'http://opensource.org/licenses/MIT'
41
+ }
42
+ }
43
+ )
44
+
45
+ type 'Status',
46
+ {
47
+ :properties => {
48
+ :status => {
49
+ :type => String,
50
+ :example => 'OK,
51
+ },
52
+ },
53
+ :required => [:status]
54
+ }
55
+
56
+ endpoint_description 'Base method to ping'
57
+ endpoint_response 200, 'Status', 'Standard response'
58
+ endpoint_tags 'Ping'
59
+ get '/' do
60
+ json({'status' => 'OK'})
61
+ end
62
+
63
+ end
64
+ ```
65
+
66
+ The swagger json endpoint will be exposed at `/swagger_doc.json`.
67
+
68
+ ## Detailed example
69
+
70
+ A more complete example is available [here](https://github.com/archiloque/sinatra-swagger-exposer/tree/master/example).
71
+
72
+ ## About swagger-ui
73
+
74
+ - If you to use [swagger-ui](https://github.com/swagger-api/swagger-ui) with your app you will need to add croo-origin setup.
75
+ The easiest way is to use the [sinatra-cross_origin](https://github.com/britg/sinatra-cross_origin) gem. Fro a simple sample you can have a look at [example application](https://github.com/archiloque/sinatra-swagger-exposer/tree/master/example).
76
+ - Swagger-ui doesn't work with all the swagger features
77
+ - Some of them like parameters maximum and minimum values are ignored
78
+ - Some of them like extending types make the endpoint unusable
79
+
80
+ ## Changes
81
+
82
+ Changelog is [here](https://github.com/archiloque/sinatra-swagger-exposer/blob/master/CHANGELOG.md).
83
+
84
+ ## Resources
85
+
86
+ - [Swagger RESTful API Documentation Specification](https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md).
87
+ - [Swagger json examples](https://github.com/swagger-api/swagger-spec/tree/master/examples/v2.0/json).
88
+ - [The swagger json schema](https://raw.githubusercontent.com/swagger-api/swagger-spec/master/schemas/v2.0/schema.json).
89
+
90
+ ## Todo
91
+
92
+ - More parameters taken into account
93
+ - More validations where possible
94
+
95
+ ## License
96
+
97
+ This software is released under the MIT license.
data/example/petstore.rb CHANGED
@@ -74,28 +74,71 @@ class Petstore < Sinatra::Base
74
74
  },
75
75
  },
76
76
  }
77
+ type 'Cat', {
78
+ # Not yet supported in swagger-ui, see https://github.com/swagger-api/swagger-js/issues/188
79
+ :extends => 'Pet',
80
+ :properties => {
81
+ :fluffy => {
82
+ :type => TrueClass,
83
+ :description => 'is this cat fluffy ?',
84
+ :example => true,
85
+ },
86
+ },
87
+ }
77
88
 
78
89
  endpoint_summary 'Finds all the pets'
79
90
  endpoint_description 'Returns all pets from the system that the user has access to'
80
91
  endpoint_tags 'Pets'
81
92
  endpoint_response 200, ['Pet'], 'Standard response'
93
+ endpoint_parameter :size, 'The number of pets to return', :query, false, Integer,
94
+ {
95
+ :example => 100,
96
+ :default => 20, # If the caller send no value the default value will be set in the params
97
+ :maximum => 100,
98
+ :minimum => 0,
99
+ :exclusiveMinimum => true,
100
+ }
82
101
  get '/pets' do
83
102
  content_type :json
84
103
  [].to_json
85
104
  end
86
105
 
106
+ endpoint_summary 'Finds all the cats'
107
+ endpoint_description 'Returns all cats from the system that the user has access to'
108
+ endpoint_tags 'Cats'
109
+ endpoint_response 200, ['Cat'], 'Standard response'
110
+ endpoint_parameter :size, 'The number of cats to return', :query, false, Integer,
111
+ {
112
+ :example => 100,
113
+ :default => 20, # If the caller send no value the default value will be set in the params
114
+ :maximum => 100,
115
+ :minimum => 0,
116
+ :exclusiveMinimum => true,
117
+ }
118
+ get '/cats' do
119
+ content_type :json
120
+ [].to_json
121
+ end
122
+
87
123
  endpoint_summary 'Finds a pet by its id'
88
124
  endpoint_description 'Finds a pet by its id, or 404 if the user does not have access to the pet'
89
125
  endpoint_tags 'Pets'
90
126
  endpoint_response 200, 'Pet', 'Standard response'
91
127
  endpoint_response 404, 'Error', 'Pet not found'
92
- endpoint_parameter :id, 'The pet id', :path, true, String
93
- {
94
- :example => 'AMZ',
95
- }
128
+ endpoint_parameter :id, 'The pet id', :path, true, Integer, # Will fail if a non-numerical value is used
129
+ {
130
+ :example => 1234,
131
+ }
96
132
  get '/pets/:id' do
97
133
  content_type :json
98
- [404, {:code => 404, :message => 'Pet not found'}]
134
+ [404, {:code => 404, :message => 'Pet not found'}.to_json]
135
+ end
136
+
137
+ # See https://github.com/britg/sinatra-cross_origin/issues/18
138
+ options '*' do
139
+ response.headers['Allow'] = 'HEAD,GET,PUT,POST,DELETE,OPTIONS'
140
+ response.headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept'
141
+ 200
99
142
  end
100
143
 
101
144
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'swagger-invalid-exception'
2
+ require_relative 'swagger-parameter-preprocessor'
2
3
  require_relative 'swagger-utilities'
3
4
 
4
5
  module Sinatra
@@ -10,8 +11,33 @@ module Sinatra
10
11
  include SwaggerUtilities
11
12
 
12
13
  HOW_TO_PASS_BODY = 'body'
13
- HOW_TO_PASS= ['path', 'query', 'header', 'formData'] + [HOW_TO_PASS_BODY]
14
- PRIMITIVE_TYPES_FOR_NON_BODY = ['string', 'number', 'integer', 'boolean']
14
+ HOW_TO_PASS_HEADER = 'header'
15
+ HOW_TO_PASS_PATH = 'path'
16
+ HOW_TO_PASS_QUERY = 'query'
17
+ HOW_TO_PASS = [HOW_TO_PASS_PATH, HOW_TO_PASS_QUERY, HOW_TO_PASS_HEADER, 'formData', HOW_TO_PASS_BODY]
18
+
19
+ TYPE_INTEGER = 'integer'
20
+ TYPE_BOOLEAN = 'boolean'
21
+ TYPE_NUMBER = 'number'
22
+ TYPE_STRING = 'string'
23
+ PRIMITIVE_TYPES_FOR_NON_BODY = [TYPE_STRING, TYPE_NUMBER, TYPE_INTEGER, TYPE_BOOLEAN]
24
+
25
+ PARAMS_FORMAT = :format
26
+ PARAMS_DEFAULT = :default
27
+ PARAMS_EXAMPLE = :example
28
+ PARAMS_MAXIMUM = :maximum
29
+ PARAMS_MINIMUM = :minimum
30
+ PARAMS_EXCLUSIVE_MINIMUM = :exclusiveMinimum
31
+ PARAMS_EXCLUSIVE_MAXIMUM = :exclusiveMaximum
32
+ PARAMS_LIST = [
33
+ PARAMS_FORMAT,
34
+ PARAMS_DEFAULT,
35
+ PARAMS_EXAMPLE,
36
+ PARAMS_MAXIMUM,
37
+ PARAMS_MINIMUM,
38
+ PARAMS_EXCLUSIVE_MINIMUM,
39
+ PARAMS_EXCLUSIVE_MAXIMUM,
40
+ ]
15
41
 
16
42
  def initialize(name, description, how_to_pass, required, type, params, known_types)
17
43
  unless name.is_a?(String) || name.is_a?(Symbol)
@@ -29,7 +55,7 @@ module Sinatra
29
55
 
30
56
  how_to_pass = how_to_pass.to_s
31
57
  unless HOW_TO_PASS.include? how_to_pass
32
- raise SwaggerInvalidException.new("Unknown how to pass value [#{how_to_pass}], registered types are #{HOW_TO_PASS.join(', ')}")
58
+ raise SwaggerInvalidException.new("Unknown how to pass value [#{how_to_pass}]#{list_or_none(HOW_TO_PASS, 'registered types')}")
33
59
  end
34
60
  @how_to_pass = how_to_pass
35
61
 
@@ -44,11 +70,20 @@ module Sinatra
44
70
  end
45
71
  @required = required
46
72
 
47
- if params
48
- white_list_params(params, [:format])
49
- end
73
+ white_list_params(params, PARAMS_LIST)
74
+ validate_params(params)
50
75
  @params = params
76
+ end
51
77
 
78
+ # Validate parameters
79
+ # @param params [Hash]
80
+ def validate_params(params)
81
+ validate_limit_parameter(params, PARAMS_MAXIMUM, PARAMS_EXCLUSIVE_MAXIMUM)
82
+ validate_limit_parameter(params, PARAMS_MINIMUM, PARAMS_EXCLUSIVE_MINIMUM)
83
+ end
84
+
85
+ def preprocessor
86
+ SwaggerParameterPreprocessor.new(@name, @how_to_pass, @required, @type, @params[:default], @params)
52
87
  end
53
88
 
54
89
  def to_swagger
@@ -65,14 +100,14 @@ module Sinatra
65
100
  if PRIMITIVE_TYPES.include? @items
66
101
  result[:items] = {:type => @items}
67
102
  else
68
- result[:schema] = {'$ref' => "#/definitions/#{@items}"}
103
+ result[:schema] = ref_to_type(@items)
69
104
  end
70
105
  end
71
106
  else
72
107
  if PRIMITIVE_TYPES.include? @type
73
108
  result[:type] = @type
74
109
  else
75
- result[:schema] = {'$ref' => "#/definitions/#{@type}"}
110
+ result[:schema] = ref_to_type(@type)
76
111
  end
77
112
  end
78
113
  end
@@ -80,7 +115,7 @@ module Sinatra
80
115
  if @description
81
116
  result[:description] = @description
82
117
  end
83
- if @params
118
+ unless @params.empty?
84
119
  result.merge!(@params)
85
120
  end
86
121
 
@@ -94,11 +129,42 @@ module Sinatra
94
129
  :required => @required,
95
130
  :type => @type,
96
131
  :items => @items,
97
- :params => @params,
98
132
  :description => @description,
133
+ :params => @params,
99
134
  }.to_json
100
135
  end
101
136
 
137
+ private
138
+
139
+ # Test if a parameter is a boolean
140
+ # @param name the parameter's name
141
+ # @value value the parameter's value
142
+ # @return [NilClass]
143
+ def check_boolean(name, value)
144
+ unless [true, false].include? value
145
+ raise SwaggerInvalidException.new("Invalid boolean value [#{value}] for [#{name}]")
146
+ end
147
+ end
148
+
149
+ # Validate a limit param like maximum and exclusiveMaximum
150
+ # @param params [Hash] the parameters
151
+ # @param limit_param_name [Symbol] the limit parameter name
152
+ # @param exclusive_limit_param_name [Symbol] the exclusive limit parameter name
153
+ def validate_limit_parameter(params, limit_param_name, exclusive_limit_param_name)
154
+ if params.key? limit_param_name
155
+ unless [TYPE_INTEGER, TYPE_NUMBER].include? @type
156
+ raise SwaggerInvalidException.new("Parameter #{limit_param_name} can only be specified for type #{TYPE_INTEGER} and #{TYPE_NUMBER} and not for [#{@type}]")
157
+ end
158
+ end
159
+
160
+ if params.key? exclusive_limit_param_name
161
+ check_boolean(PARAMS_EXCLUSIVE_MINIMUM, params[exclusive_limit_param_name])
162
+ unless params.key? limit_param_name
163
+ raise SwaggerInvalidException.new("You can't have a #{exclusive_limit_param_name} value without a #{limit_param_name}")
164
+ end
165
+ end
166
+ end
167
+
102
168
  end
103
169
 
104
170
  end
@@ -26,7 +26,7 @@ module Sinatra
26
26
  if PRIMITIVE_TYPES.include? @items
27
27
  schema[:items] = {:type => @items}
28
28
  else
29
- schema[:items] = {'$ref' => "#/definitions/#{@items}"}
29
+ schema[:items] = ref_to_type(@items)
30
30
  end
31
31
  end
32
32
  result[:schema] = schema
@@ -34,7 +34,7 @@ module Sinatra
34
34
  if PRIMITIVE_TYPES.include? @type
35
35
  result[:schema] = {:type => @type}
36
36
  else
37
- result[:schema] = {'$ref' => "#/definitions/#{@type}"}
37
+ result[:schema] = ref_to_type(@type)
38
38
  end
39
39
  end
40
40
  end
@@ -1,3 +1,4 @@
1
+ require_relative 'swagger-request-preprocessor'
1
2
  require_relative 'swagger-utilities'
2
3
 
3
4
  module Sinatra
@@ -9,13 +10,21 @@ module Sinatra
9
10
 
10
11
  include SwaggerUtilities
11
12
 
12
- attr_reader :path, :type
13
+ attr_reader :path, :type, :request_preprocessor
13
14
 
14
15
  def initialize(type, path, parameters, responses, summary, description, tags)
15
16
  @type = type
16
17
  @path = sinatra_path_to_swagger_path(path)
18
+ @request_preprocessor = SwaggerRequestPreprocessor.new
17
19
 
18
20
  @parameters = parameters
21
+ @parameters.each do |parameter|
22
+ preprocessor = parameter.preprocessor
23
+ if preprocessor.useful?
24
+ @request_preprocessor.add_preprocessor preprocessor
25
+ end
26
+ end
27
+
19
28
  @responses = responses
20
29
 
21
30
  @attributes = {}
@@ -21,13 +21,15 @@ module Sinatra
21
21
  app.set :swagger_current_endpoint_parameters, {}
22
22
  app.set :swagger_current_endpoint_responses, {}
23
23
  app.set :swagger_types, {}
24
+ declare_swagger_endpoints(app)
25
+ end
24
26
 
25
- # Declare the swagger endpoints
27
+ def self.declare_swagger_endpoints(app)
26
28
  app.endpoint_summary 'The swagger endpoint'
27
29
  app.endpoint_tags 'swagger'
28
30
  app.get '/swagger_doc.json' do
29
31
  swagger_content = ::Sinatra::SwaggerExposer::SwaggerContentCreator.new(
30
- settings.respond_to?(:swagger_info) ? settings.swagger_info : nil ,
32
+ settings.respond_to?(:swagger_info) ? settings.swagger_info : nil,
31
33
  settings.swagger_types,
32
34
  settings.swagger_endpoints
33
35
  ).to_swagger
@@ -40,26 +42,25 @@ module Sinatra
40
42
  app.options '/swagger_doc.json' do
41
43
  200
42
44
  end
43
-
44
45
  end
45
46
 
46
47
  # Provide a summary for the endpoint
47
48
  def endpoint_summary(summary)
48
- set_if_type_and_not_exist(summary, 'summary', String)
49
+ set_if_type_and_not_exist(summary, :summary, String)
49
50
  end
50
51
 
51
52
  # Provide a description for the endpoint
52
53
  def endpoint_description(description)
53
- set_if_type_and_not_exist(description, 'description', String)
54
+ set_if_type_and_not_exist(description, :description, String)
54
55
  end
55
56
 
56
57
  # Provide tags for the endpoint
57
58
  def endpoint_tags(*tags)
58
- set_if_type_and_not_exist(tags, 'tags', nil)
59
+ set_if_type_and_not_exist(tags, :tags, nil)
59
60
  end
60
61
 
61
62
  # Define parameter for the endpoint
62
- def endpoint_parameter(name, description, how_to_pass, required, type, params = nil)
63
+ def endpoint_parameter(name, description, how_to_pass, required, type, params = {})
63
64
  parameters = settings.swagger_current_endpoint_parameters
64
65
  check_if_not_duplicate(name, parameters, 'Parameter')
65
66
  parameters[name] = SwaggerEndpointParameter.new(
@@ -93,70 +94,38 @@ module Sinatra
93
94
  responses[code] = SwaggerEndpointResponse.new(type, description, settings.swagger_types.keys)
94
95
  end
95
96
 
96
- def delete(*args, &block)
97
- process_endpoint('delete', args)
98
- super(*args, &block)
99
- end
100
-
101
- def get(*args, &block)
102
- process_endpoint('get', args)
103
- super(*args, &block)
104
- end
105
-
106
- def head(*args, &block)
107
- process_endpoint('head', args)
108
- super(*args, &block)
109
- end
110
-
111
- def link(*args, &block)
112
- process_endpoint('link', args)
113
- super(*args, &block)
114
- end
115
-
116
- def options(*args, &block)
117
- process_endpoint('options', args)
118
- super(*args, &block)
119
- end
120
-
121
- def patch(*args, &block)
122
- process_endpoint('patch', args)
123
- super(*args, &block)
124
- end
125
-
126
- def post(*args, &block)
127
- process_endpoint('post', args)
128
- super(*args, &block)
129
- end
130
-
131
- def put(*args, &block)
132
- process_endpoint('put', args)
133
- super(*args, &block)
134
- end
135
-
136
- def unlink(*args, &block)
137
- process_endpoint('unlink', args)
138
- super(*args, &block)
97
+ def route(verb, path, options = {}, &block)
98
+ if verb == 'HEAD'
99
+ super(verb, path, options, &block)
100
+ else
101
+ request_preprocessor = process_endpoint(verb.downcase, path, options)
102
+ super(verb, path, options) do
103
+ request_preprocessor.run(self, &block)
104
+ end
105
+ end
139
106
  end
140
107
 
141
108
  private
142
109
 
143
110
  # Call for each endpoint declaration
144
- def process_endpoint(type, args)
111
+ # @return [SwaggerRequestPreprocessor]
112
+ def process_endpoint(type, path, opts)
145
113
  current_endpoint_info = settings.swagger_current_endpoint_info
146
114
  current_endpoint_parameters = settings.swagger_current_endpoint_parameters
147
115
  current_endpoint_responses = settings.swagger_current_endpoint_responses
148
- endpoint_path = args[0]
149
- settings.swagger_endpoints << SwaggerEndpoint.new(
116
+ endpoint = SwaggerEndpoint.new(
150
117
  type,
151
- endpoint_path,
118
+ path,
152
119
  current_endpoint_parameters.values,
153
120
  current_endpoint_responses.clone,
154
121
  current_endpoint_info[:summary],
155
122
  current_endpoint_info[:description],
156
123
  current_endpoint_info[:tags])
124
+ settings.swagger_endpoints << endpoint
157
125
  current_endpoint_info.clear
158
126
  current_endpoint_parameters.clear
159
127
  current_endpoint_responses.clear
128
+ endpoint.request_preprocessor
160
129
  end
161
130
 
162
131
  def set_if_type_and_not_exist(value, name, type)
@@ -1,4 +1,5 @@
1
1
  require_relative 'swagger-invalid-exception'
2
+ require_relative 'swagger-utilities'
2
3
 
3
4
  module Sinatra
4
5
 
@@ -7,6 +8,8 @@ module Sinatra
7
8
  # The info declaration
8
9
  class SwaggerInfo
9
10
 
11
+ include SwaggerUtilities
12
+
10
13
  def initialize(values)
11
14
  @values = process(values, 'info', INFO_FIELDS, values)
12
15
  end
@@ -47,7 +50,7 @@ module Sinatra
47
50
  end
48
51
  end
49
52
  else
50
- raise SwaggerInvalidException.new("Unknown property [#{current_key}] for #{current_field_name}, possible keys are: #{current_fields.keys.join(', ')}: #{top_level_hash}")
53
+ raise SwaggerInvalidException.new("Unknown property [#{current_key}] for #{current_field_name}#{list_or_none(current_fields.keys, 'values')}")
51
54
  end
52
55
  end
53
56
  result.empty? ? nil : result
@@ -0,0 +1,139 @@
1
+ require_relative 'swagger-endpoint-parameter'
2
+ require_relative 'swagger-invalid-exception'
3
+
4
+ module Sinatra
5
+
6
+ module SwaggerExposer
7
+
8
+ # Process the parameters for validation and enrichment
9
+ class SwaggerParameterPreprocessor
10
+
11
+ def initialize(name, how_to_pass, required, type, default, params)
12
+ @name = name.to_s
13
+ @how_to_pass = how_to_pass
14
+ @required = required
15
+ @type = type
16
+ @default = default
17
+ @params = params
18
+
19
+ # All headers are upcased
20
+ if how_to_pass == SwaggerEndpointParameter::HOW_TO_PASS_HEADER
21
+ @name.upcase!
22
+ end
23
+ end
24
+
25
+ def useful?
26
+ @required || (!@default.nil?) || [SwaggerEndpointParameter::TYPE_NUMBER, SwaggerEndpointParameter::TYPE_INTEGER, SwaggerEndpointParameter::TYPE_BOOLEAN].include?(@type)
27
+ end
28
+
29
+ def run(app, parsed_body)
30
+ case @how_to_pass
31
+ when SwaggerEndpointParameter::HOW_TO_PASS_QUERY, SwaggerEndpointParameter::HOW_TO_PASS_PATH
32
+ check_param(app.params)
33
+ when SwaggerEndpointParameter::HOW_TO_PASS_HEADER
34
+ check_param(app.headers)
35
+ when SwaggerEndpointParameter::HOW_TO_PASS_BODY
36
+ check_param(parsed_body || {})
37
+ end
38
+ end
39
+
40
+ def check_param(params)
41
+ if params.key?(@name)
42
+ params[@name] = validate_param_value(params[@name])
43
+ elsif @required
44
+ raise SwaggerInvalidException.new("Mandatory parameter [#{@name}] is missing")
45
+ elsif @default
46
+ params[@name] = @default
47
+ end
48
+ params
49
+ end
50
+
51
+ def validate_param_value(value)
52
+ case @type
53
+ when SwaggerEndpointParameter::TYPE_NUMBER
54
+ return validate_param_value_number(value)
55
+ when SwaggerEndpointParameter::TYPE_INTEGER
56
+ return validate_param_value_integer(value)
57
+ when SwaggerEndpointParameter::TYPE_BOOLEAN
58
+ return validate_param_value_boolean(value)
59
+ end
60
+ end
61
+
62
+ def validate_param_value_boolean(value)
63
+ if (value == 'true') || value.is_a?(TrueClass)
64
+ return true
65
+ elsif (value == 'false') || value.is_a?(FalseClass)
66
+ return false
67
+ else
68
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be an boolean but is [#{value}]")
69
+ end
70
+ end
71
+
72
+ def validate_param_value_integer(value)
73
+ begin
74
+ f = Float(value)
75
+ i = Integer(value)
76
+ if f == i
77
+ i
78
+ else
79
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be an integer but is [#{value}]")
80
+ end
81
+ value = Integer(value)
82
+ validate_numerical_value(value)
83
+ value
84
+ rescue ArgumentError
85
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be an integer but is [#{value}]")
86
+ rescue TypeError
87
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be an integer but is [#{value}]")
88
+ end
89
+ end
90
+
91
+ def validate_param_value_number(value)
92
+ begin
93
+ value = Float(value)
94
+ validate_numerical_value(value)
95
+ return value
96
+ rescue ArgumentError
97
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be a float but is [#{value}]")
98
+ rescue TypeError
99
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be a float but is [#{value}]")
100
+ end
101
+ end
102
+
103
+ # Validate a numerical value
104
+ # @param value [Numeric] the value
105
+ def validate_numerical_value(value)
106
+ validate_numerical_value_internal(
107
+ value,
108
+ SwaggerEndpointParameter::PARAMS_MINIMUM,
109
+ SwaggerEndpointParameter::PARAMS_EXCLUSIVE_MINIMUM,
110
+ '>=',
111
+ '>')
112
+ validate_numerical_value_internal(
113
+ value,
114
+ SwaggerEndpointParameter::PARAMS_MAXIMUM,
115
+ SwaggerEndpointParameter::PARAMS_EXCLUSIVE_MAXIMUM,
116
+ '<=',
117
+ '<')
118
+ end
119
+
120
+ # Validate the value of a number
121
+ # @params value the value to check
122
+ # @params limit_param_name [Symbol] the param that contain the value to compare to
123
+ # @params exclusive_limit_param_name [Symbol] the param that indicates if the comparison is absolute
124
+ # @params limit_param_method [String] the comparison method to call
125
+ # @params exclusive_limit_param_method [String] the absolute comparison method to call
126
+ def validate_numerical_value_internal(value, limit_param_name, exclusive_limit_param_name, limit_param_method, exclusive_limit_param_method)
127
+ if @params.key? limit_param_name
128
+ target_value = @params[limit_param_name]
129
+ method_to_call = @params[exclusive_limit_param_name] ? exclusive_limit_param_method : limit_param_method
130
+ unless value.send(method_to_call, target_value)
131
+ raise SwaggerInvalidException.new("Parameter [#{@name}] should be #{method_to_call} than [#{target_value}] but is [#{value}]")
132
+ end
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,52 @@
1
+ require 'json'
2
+
3
+ require_relative 'swagger-invalid-exception'
4
+
5
+ module Sinatra
6
+
7
+ module SwaggerExposer
8
+
9
+ # A preprocessor for a request, apply the parameter preprocessor then execute the query code
10
+ class SwaggerRequestPreprocessor
11
+
12
+ attr_reader :preprocessors
13
+
14
+ def initialize
15
+ @preprocessors = []
16
+ end
17
+
18
+ def add_preprocessor(preprocessor)
19
+ @preprocessors << preprocessor
20
+ end
21
+
22
+ def run(app, &block)
23
+ parsed_body = {}
24
+ if app.env['CONTENT_TYPE'] == 'application/json'
25
+ body = app.request.body
26
+ unless body.empty?
27
+ parsed_body = JSON.parse(body)
28
+ end
29
+ end
30
+ app.params['parsed_body'] = parsed_body
31
+ unless @preprocessors.empty?
32
+ @preprocessors.each do |preprocessor|
33
+ begin
34
+ preprocessor.run(app, parsed_body)
35
+ rescue SwaggerInvalidException => e
36
+ app.content_type :json
37
+ return [400, {:code => 400, :message => e.message}.to_json]
38
+ end
39
+ end
40
+ end
41
+ if block
42
+ # Execute the block in the context of the app
43
+ app.instance_eval(&block)
44
+ else
45
+ ''
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
@@ -42,7 +42,7 @@ module Sinatra
42
42
  if PRIMITIVE_TYPES.include? @items
43
43
  result[:items] = {:type => @items}
44
44
  else
45
- result[:items] = {'$ref' => "#/definitions/#{@items}"}
45
+ result[:items] = ref_to_type(@items)
46
46
  end
47
47
  end
48
48
  else
@@ -11,19 +11,27 @@ module Sinatra
11
11
 
12
12
  include SwaggerUtilities
13
13
 
14
- def initialize(type_name, type_content, known_types)
15
- @properties = process_properties(type_name, type_content, known_types)
16
- @required = process_required(type_name, type_content, @properties.keys)
17
- @example = process_example(type_name, type_content, @properties.keys)
14
+ PROPERTY_PROPERTIES = :properties
15
+ PROPERTY_REQUIRED = :required
16
+ PROPERTY_EXAMPLE = :example
17
+ PROPERTY_EXTENDS = :extends
18
+ PROPERTIES = [PROPERTY_PROPERTIES, PROPERTY_REQUIRED, PROPERTY_EXAMPLE, PROPERTY_EXTENDS]
19
+
20
+ def initialize(type_name, type_properties, known_types)
21
+ white_list_params(type_properties, PROPERTIES)
22
+ @properties = process_properties(type_name, type_properties, known_types)
23
+ @required = process_required(type_name, type_properties, @properties.keys)
24
+ @example = process_example(type_name, type_properties, @properties.keys)
25
+ @extends = process_extends(type_properties, known_types)
18
26
  end
19
27
 
20
28
  def process_properties(type_name, type_content, known_types)
21
- possible_value = check_attribute_empty_or_bad(type_name, type_content, :properties, Hash)
29
+ possible_value = check_attribute_empty_or_bad(type_name, type_content, PROPERTY_PROPERTIES, Hash)
22
30
  if possible_value
23
31
  possible_value
24
32
  else
25
33
  result = {}
26
- type_content[:properties].each_pair do |property_name, property_properties|
34
+ type_content[PROPERTY_PROPERTIES].each_pair do |property_name, property_properties|
27
35
  result[property_name.to_s] = SwaggerTypeProperty.new(type_name, property_name, property_properties, known_types)
28
36
  end
29
37
  result
@@ -31,32 +39,32 @@ module Sinatra
31
39
  end
32
40
 
33
41
  def process_required(type_name, type_content, properties_names)
34
- possible_value = check_attribute_empty_or_bad(type_name, type_content, :required, Array)
42
+ possible_value = check_attribute_empty_or_bad(type_name, type_content, PROPERTY_REQUIRED, Array)
35
43
  if possible_value
36
44
  possible_value
37
45
  else
38
- type_content[:required].each do |property_name|
46
+ type_content[PROPERTY_REQUIRED].each do |property_name|
39
47
  property_name = property_name.to_s
40
48
  unless properties_names.include? property_name
41
- raise SwaggerInvalidException.new("Required property [#{property_name}] of [#{type_name}] is unknown, known properties: #{properties_names.join(', ')}")
49
+ raise SwaggerInvalidException.new("Required property [#{property_name}] of [#{type_name}] is unknown#{list_or_none(properties_names, 'properties')}")
42
50
  end
43
51
  end
44
- type_content[:required]
52
+ type_content[PROPERTY_REQUIRED]
45
53
  end
46
54
  end
47
55
 
48
56
  def process_example(type_name, type_content, properties_names)
49
- possible_value = check_attribute_empty_or_bad(type_name, type_content, :example, Hash)
57
+ possible_value = check_attribute_empty_or_bad(type_name, type_content, PROPERTY_EXAMPLE, Hash)
50
58
  if possible_value
51
59
  possible_value
52
60
  else
53
- type_content[:example].each_pair do |property_name, property_value|
61
+ type_content[PROPERTY_EXAMPLE].each_pair do |property_name, property_value|
54
62
  property_name = property_name.to_s
55
63
  unless properties_names.include? property_name
56
- raise SwaggerInvalidException.new("Example property [#{property_name}] with value [#{property_value}] of [#{type_name}] is unknown, known properties: #{properties_names.join(', ')}")
64
+ raise SwaggerInvalidException.new("Example property [#{property_name}] with value [#{property_value}] of [#{type_name}] is unknown#{list_or_none(properties_names, 'properties')}")
57
65
  end
58
66
  end
59
- type_content[:example]
67
+ type_content[PROPERTY_EXAMPLE]
60
68
  end
61
69
  end
62
70
 
@@ -68,19 +76,35 @@ module Sinatra
68
76
  end
69
77
  end
70
78
 
79
+ def process_extends(type_properties, known_types)
80
+ if type_properties.key? PROPERTY_EXTENDS
81
+ check_type(type_properties[PROPERTY_EXTENDS], known_types)
82
+ @extends = type_properties[PROPERTY_EXTENDS]
83
+ end
84
+ end
85
+
71
86
  def to_swagger
72
- result = {}
87
+ result = {:type => 'object'}
73
88
 
74
89
  unless @properties.empty?
75
- result[:properties] = hash_to_swagger(@properties)
90
+ result[PROPERTY_PROPERTIES] = hash_to_swagger(@properties)
76
91
  end
77
92
 
78
93
  unless @required.empty?
79
- result[:required] = @required
94
+ result[PROPERTY_REQUIRED] = @required
80
95
  end
81
96
 
82
97
  unless @example.empty?
83
- result[:example] = @example
98
+ result[PROPERTY_EXAMPLE] = @example
99
+ end
100
+
101
+ if @extends
102
+ result = {
103
+ :allOf => [
104
+ ref_to_type(@extends),
105
+ result
106
+ ]
107
+ }
84
108
  end
85
109
 
86
110
  result
@@ -16,9 +16,13 @@ module Sinatra
16
16
  'boolean',
17
17
  'date',
18
18
  'dateTime',
19
- 'password'
19
+ 'password',
20
20
  ]
21
21
 
22
+ def ref_to_type(type)
23
+ {'$ref' => "#/definitions/#{type}"}
24
+ end
25
+
22
26
  def hash_to_swagger(hash)
23
27
  result = {}
24
28
  hash.each_pair do |key, value|
@@ -27,8 +31,12 @@ module Sinatra
27
31
  result
28
32
  end
29
33
 
34
+ # Transform a type into a String
35
+ # @return [String]
30
36
  def type_to_s(value)
31
- if value.is_a? Class
37
+ if [TrueClass, FalseClass].include? value
38
+ 'boolean'
39
+ elsif value.is_a? Class
32
40
  value.to_s.downcase
33
41
  else
34
42
  value
@@ -51,14 +59,26 @@ module Sinatra
51
59
  end
52
60
  end
53
61
 
54
- def white_list_params(params, allowed_params)
62
+ # Validate if a parameter is in a list of available values
63
+ # @param params the parameter
64
+ # @param allowed_values [Enumerable, #include?] the allowed values
65
+ # @return [NilClass]
66
+ def white_list_params(params, allowed_values)
55
67
  params.each_pair do |key, value|
56
- unless allowed_params.include? key
57
- raise SwaggerInvalidException.new("Unknown property [#{key}] for with value [#{value}], known properties are #{allowed_params.join(', ')}")
68
+ unless allowed_values.include? key
69
+ raise SwaggerInvalidException.new("Unknown property [#{key}] with value [#{value}]#{list_or_none(allowed_values, 'properties')}")
58
70
  end
59
71
  end
60
72
  end
61
73
 
74
+ def list_or_none(list, name)
75
+ if list.empty?
76
+ ", no available #{name}"
77
+ else
78
+ ", possible #{name} are #{list.join(', ')}"
79
+ end
80
+ end
81
+
62
82
  private
63
83
 
64
84
  def get_array_type(array)
@@ -71,9 +91,15 @@ module Sinatra
71
91
  end
72
92
  end
73
93
 
74
- def check_type(type, possible_values)
75
- unless possible_values.include? type
76
- raise SwaggerInvalidException.new("Unknown type [#{type}], possible types are #{possible_values.join(', ')}")
94
+ # Validate if a type is in a list of available values
95
+ # @param type [String] the parameter
96
+ # @param allowed_values [Enumerable, #include?] the allowed values
97
+ # @return [NilClass]
98
+ def check_type(type, allowed_values)
99
+ if allowed_values.empty?
100
+ raise SwaggerInvalidException.new("Unknown type [#{type}], no available type")
101
+ elsif !allowed_values.include?(type)
102
+ raise SwaggerInvalidException.new("Unknown type [#{type}]#{list_or_none(allowed_values, 'types')}")
77
103
  end
78
104
  end
79
105
 
@@ -1,5 +1,5 @@
1
1
  module Sinatra
2
2
  module SwaggerExposer
3
- VERSION = '0.0.1'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
  end
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Julien Kirch']
10
10
 
11
11
  spec.summary = %q{Expose swagger API from your Sinatra app}
12
- spec.description = %q{This Sinatra extension enable you to add metadata to your code and to expose your API as a Swagger endpoint}
12
+ spec.description = %q{This Sinatra extension enable you to add metadata to your code to expose your API as a Swagger endpoint and to validate and enrich the invocation parameters}
13
13
  spec.homepage = 'https://github.com/archiloque/sinatra-swagger-exposer'
14
14
  spec.license = 'MIT'
15
15
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-swagger-exposer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julien Kirch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-05 00:00:00.000000000 Z
11
+ date: 2015-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
@@ -94,8 +94,8 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
- description: This Sinatra extension enable you to add metadata to your code and to
98
- expose your API as a Swagger endpoint
97
+ description: This Sinatra extension enable you to add metadata to your code to expose
98
+ your API as a Swagger endpoint and to validate and enrich the invocation parameters
99
99
  email:
100
100
  executables: []
101
101
  extensions: []
@@ -103,10 +103,11 @@ extra_rdoc_files: []
103
103
  files:
104
104
  - ".gitignore"
105
105
  - ".travis.yml"
106
+ - CHANGELOG.md
106
107
  - CODE_OF_CONDUCT.md
107
108
  - Gemfile
108
109
  - LICENSE.txt
109
- - README.asciidoc
110
+ - README.md
110
111
  - Rakefile
111
112
  - example/Gemfile
112
113
  - example/Gemfile.lock
@@ -119,6 +120,8 @@ files:
119
120
  - lib/sinatra/swagger-exposer/swagger-exposer.rb
120
121
  - lib/sinatra/swagger-exposer/swagger-info.rb
121
122
  - lib/sinatra/swagger-exposer/swagger-invalid-exception.rb
123
+ - lib/sinatra/swagger-exposer/swagger-parameter-preprocessor.rb
124
+ - lib/sinatra/swagger-exposer/swagger-request-preprocessor.rb
122
125
  - lib/sinatra/swagger-exposer/swagger-type-property.rb
123
126
  - lib/sinatra/swagger-exposer/swagger-type.rb
124
127
  - lib/sinatra/swagger-exposer/swagger-utilities.rb
data/README.asciidoc DELETED
@@ -1,75 +0,0 @@
1
- # Sinatra::SwaggerExposer
2
-
3
- image:https://codeclimate.com/github/archiloque/sinatra-swagger-exposer/badges/gpa.svg["Code status", link=https://codeclimate.com/github/archiloque/sinatra-swagger-exposer]
4
- image:https://travis-ci.org/archiloque/sinatra-swagger-exposer.svg?branch=master["Build Status", link="https://travis-ci.org/archiloque/sinatra-swagger-exposer"]
5
- image:https://coveralls.io/repos/archiloque/sinatra-swagger-exposer/badge.svg?branch=master["Coverage Status", link="https://coveralls.io/r/archiloque/sinatra-swagger-exposer?branch=master"]
6
-
7
- Create Swagger endpoint for your Sinatra application.
8
-
9
- This Sinatra extension enable you to add metadata to your code and to expose your API as a Swagger endpoint.
10
-
11
- I'm adding features as I need them and it currently doesn't use all the Swagger options, so if you need one that is missing please open an issue or contribute.
12
-
13
- ## Design choices
14
-
15
- - All the declarations are validated when the server is started
16
- - The declarations are defined to look as ruby-ish as possible
17
-
18
- To use it in your app :
19
-
20
- [source,ruby]
21
- ----
22
- require 'sinatra/swagger-exposer/swagger-exposer'
23
-
24
- class MyApp < Sinatra::Base
25
-
26
- register Sinatra::SwaggerExposer
27
-
28
- general_info(
29
- {
30
- version: '0.0.1',
31
- title: 'May app',
32
- description: 'My wonderful app',
33
- license: {
34
- name: 'MIT',
35
- url: 'http://opensource.org/licenses/MIT'
36
- }
37
- }
38
- )
39
-
40
- type 'Status',
41
- {
42
- :properties => {
43
- :status => {
44
- :type => String,
45
- :example => 'OK,
46
- },
47
- },
48
- :required => [:status]
49
- }
50
-
51
- endpoint_description 'Base method to ping'
52
- endpoint_response 200, 'Standard response', 'Status'
53
- endpoint_tags 'Ping'
54
- get '/' do
55
- json({'status' => 'OK'})
56
- end
57
-
58
- end
59
- ----
60
-
61
- The swagger json endpoint will be exposed at `/swagger_doc.json`.
62
-
63
- ## Detailed example
64
-
65
- A more complete example is available link:https://github.com/archiloque/sinatra-swagger-exposer/tree/master/example[here].
66
-
67
- ## Resources
68
-
69
- - link:https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md[Swagger RESTful API Documentation Specification].
70
- - link:https://github.com/swagger-api/swagger-spec/tree/master/examples/v2.0/json[Swagger json examples].
71
- - link:https://raw.githubusercontent.com/swagger-api/swagger-spec/master/schemas/v2.0/schema.json[The swagger json schema].
72
-
73
- ## License
74
-
75
- This software is released under the MIT license.