sinatra-swagger-exposer 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.