openapi_rest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/generators/openapi_rest/install_generator.rb +11 -0
- data/lib/generators/openapi_rest/openapi_doc.rb +3 -0
- data/lib/generators/templates/api_docs.yml +162 -0
- data/lib/openapi_rest.rb +22 -0
- data/lib/openapi_rest/api_config.rb +12 -0
- data/lib/openapi_rest/api_doc.rb +19 -0
- data/lib/openapi_rest/api_doc_parser.rb +110 -0
- data/lib/openapi_rest/api_model.rb +36 -0
- data/lib/openapi_rest/api_parameters.rb +76 -0
- data/lib/openapi_rest/api_validator.rb +20 -0
- data/lib/openapi_rest/extension.rb +35 -0
- data/lib/openapi_rest/operations/filter.rb +24 -0
- data/lib/openapi_rest/operations/paginate.rb +19 -0
- data/lib/openapi_rest/operations/sort.rb +27 -0
- data/lib/openapi_rest/query_builder.rb +70 -0
- data/lib/openapi_rest/query_response.rb +89 -0
- data/lib/openapi_rest/railtie.rb +9 -0
- data/lib/openapi_rest/rest_renderer.rb +75 -0
- data/lib/openapi_rest/validators/format.rb +24 -0
- data/lib/openapi_rest/validators/pattern.rb +21 -0
- data/lib/openapi_rest/version.rb +3 -0
- data/test/openapi_rest/api_config_test.rb +9 -0
- data/test/openapi_rest/api_doc_parser_test.rb +71 -0
- data/test/openapi_rest/api_doc_test.rb +15 -0
- data/test/openapi_rest/api_model_test.rb +49 -0
- data/test/openapi_rest/api_parameters_test.rb +57 -0
- data/test/openapi_rest/api_validator_test.rb +23 -0
- data/test/openapi_rest/query_builder_test.rb +32 -0
- data/test/openapi_rest/query_response_test.rb +129 -0
- data/test/spec_helper.rb +8 -0
- data/test/support/data.rb +3 -0
- data/test/support/models.rb +3 -0
- data/test/support/schema.rb +13 -0
- metadata +139 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Api doc parser based on OpenAPI 2.0 specs
|
4
|
+
#
|
5
|
+
class ApiDocParser
|
6
|
+
attr_reader :method
|
7
|
+
attr_reader :document
|
8
|
+
|
9
|
+
def initialize(openapi_path)
|
10
|
+
@document = OpenAPIRest::ApiDoc.document
|
11
|
+
@route = openapi_path[:path]
|
12
|
+
@method = openapi_path[:method]
|
13
|
+
@method = 'patch' if @method == 'put'
|
14
|
+
end
|
15
|
+
|
16
|
+
def definitions
|
17
|
+
@current_step = document.fetch('definitions', {})
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def parameters
|
22
|
+
@current_step = document.fetch('parameters', {})
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def paths
|
27
|
+
@current_step = document.fetch('paths', {})
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def find(key)
|
32
|
+
@current_step = @current_step.fetch(key, {})
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def base_path
|
37
|
+
document.fetch('basePath', {})
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_path
|
41
|
+
paths.find(@route.sub(base_path, '')).find(method)
|
42
|
+
end
|
43
|
+
|
44
|
+
def properties
|
45
|
+
@current_step = @current_step.fetch('properties', {})
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def schema
|
50
|
+
@current_step = @current_step.fetch('schema', {})
|
51
|
+
|
52
|
+
if !@current_step['$ref'].nil?
|
53
|
+
if @current_step['$ref'].include?('#/definitions/')
|
54
|
+
str = @current_step['$ref'].gsub('#/definitions/', '')
|
55
|
+
return definitions.find(str)
|
56
|
+
end
|
57
|
+
elsif !@current_step['items'].nil?
|
58
|
+
if @current_step['items']['$ref'].include?('#/definitions/')
|
59
|
+
str = @current_step['items']['$ref'].gsub('#/definitions/', '')
|
60
|
+
return definitions.find(str)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_parameters
|
68
|
+
return if @current_step['parameters'].nil?
|
69
|
+
|
70
|
+
params = {}
|
71
|
+
ref_params = []
|
72
|
+
@current_step['parameters'].each do |parameter|
|
73
|
+
next if parameter['in'] == 'path'
|
74
|
+
if parameter['in'] == 'query' || parameter['in'] == 'body'
|
75
|
+
params[parameter['name']] = parameter['name']
|
76
|
+
next
|
77
|
+
end
|
78
|
+
|
79
|
+
if !parameter['$ref'].nil? && parameter['$ref'].include?('#/parameters/')
|
80
|
+
param = parameter['$ref'].gsub('#/parameters/', '')
|
81
|
+
ref_params << document.fetch('parameters', {}).fetch(param, {})
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
if ref_params.length > 0
|
86
|
+
params.merge!(ref_params.compact.map { |param| param.fetch('schema', {}).fetch('properties', {}) }.first)
|
87
|
+
end
|
88
|
+
|
89
|
+
params
|
90
|
+
end
|
91
|
+
|
92
|
+
def responses
|
93
|
+
@current_step = @current_step.fetch('responses', {})
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def keys
|
98
|
+
@current_step.keys
|
99
|
+
end
|
100
|
+
|
101
|
+
def [](key)
|
102
|
+
@current_step = @current_step.fetch(key, {})
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_s
|
107
|
+
@current_step
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Rest Api Model
|
4
|
+
#
|
5
|
+
class ApiModel
|
6
|
+
attr_reader :type
|
7
|
+
attr_accessor :model
|
8
|
+
|
9
|
+
def initialize(type)
|
10
|
+
@type = type
|
11
|
+
@model = type.to_s.capitalize!.constantize
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(params, args = {}, &block)
|
15
|
+
native_query(params.merge(operation: :create), args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def where(params, args = {}, &block)
|
19
|
+
native_query(params.merge(operation: :query), args, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find(params, args = {}, &block)
|
23
|
+
native_query(params.merge(operation: :squery), args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def native_query(params, args)
|
29
|
+
query_builder = OpenAPIRest::QueryBuilder.new(self, params.merge(query: args))
|
30
|
+
|
31
|
+
yield(self) if block_given?
|
32
|
+
|
33
|
+
query_builder.response
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Rest Api Parameters
|
4
|
+
#
|
5
|
+
class ApiParameters
|
6
|
+
attr_reader :allowed_params, :validation_errors
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@params = args.fetch(:params, {})
|
10
|
+
@model_class = args.fetch(:api_model).model
|
11
|
+
@doc_parser = OpenAPIRest::ApiDocParser.new(args.fetch(:openapi_path, {}))
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
@validation_errors = []
|
16
|
+
|
17
|
+
validate
|
18
|
+
|
19
|
+
@validation_errors.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def response_permitted_params
|
23
|
+
@doc_parser.find_path.responses.find(response_code).schema.properties.keys.collect(&:to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def response_code
|
29
|
+
return 204 if @doc_parser.method.to_sym == :patch || @doc_parser.method.to_sym == :delete
|
30
|
+
return 201 if @doc_parser.method.to_sym == :post
|
31
|
+
200
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate
|
35
|
+
if !@params.include?(property_param) || @params[property_param].empty?
|
36
|
+
@validation_errors << ["Missing Parameter: #{property_param}"]
|
37
|
+
return @validation_errors
|
38
|
+
end
|
39
|
+
|
40
|
+
@allowed_params = @params.require(property_param).permit(save_permitted_params)
|
41
|
+
@validation_errors = @allowed_params.keys.map do |key|
|
42
|
+
OpenAPIRest::ApiValidator.new(root_parameters[key.to_s]).evaluate(key, @allowed_params)
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def save_permitted_params
|
47
|
+
root_parameters.keys.collect(&:to_sym)
|
48
|
+
end
|
49
|
+
|
50
|
+
def root_parameters
|
51
|
+
params = @doc_parser.find_path.find_parameters
|
52
|
+
puts 'ERROR: parameters not found' if params.nil?
|
53
|
+
params
|
54
|
+
end
|
55
|
+
|
56
|
+
def property
|
57
|
+
@model_class.name.demodulize
|
58
|
+
end
|
59
|
+
|
60
|
+
def property_param
|
61
|
+
if @model_class.name.nil?
|
62
|
+
@model_class.class.name.demodulize.downcase.to_sym
|
63
|
+
else
|
64
|
+
@model_class.name.demodulize.downcase.to_sym
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def api_parameters(key)
|
69
|
+
@doc_parser.document.parameters.find(key)
|
70
|
+
end
|
71
|
+
|
72
|
+
def api_definitions(key)
|
73
|
+
@doc_parser.document.definitions.find(key)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Rest api validator
|
4
|
+
#
|
5
|
+
class ApiValidator
|
6
|
+
def initialize(parameter)
|
7
|
+
@parameter = parameter
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate(key, value)
|
11
|
+
if @parameter['format'].present?
|
12
|
+
validator = OpenAPIRest::Validators::Format.new(@parameter['format'], value[key])
|
13
|
+
return validator.error(key) unless validator.valid?
|
14
|
+
elsif @parameter['pattern'].present?
|
15
|
+
validator = OpenAPIRest::Validators::Pattern.new(@parameter['pattern'], value[key])
|
16
|
+
return validator.error(key) unless validator.valid?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Rest Extension
|
4
|
+
#
|
5
|
+
module Extension
|
6
|
+
module ClassMethods #:nodoc:
|
7
|
+
def self.included(clazz)
|
8
|
+
clazz.send(:before_filter, :retrieve_openapi_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :openapi_path
|
12
|
+
|
13
|
+
def retrieve_openapi_path
|
14
|
+
all_routes = Rails.application.routes.routes
|
15
|
+
path = request.path
|
16
|
+
nspace = ''
|
17
|
+
all_routes.each do |r|
|
18
|
+
next unless r.defaults.fetch(:openapi, false) && Regexp.new(r.verb).match(request.method)
|
19
|
+
|
20
|
+
match = Regexp.new(r.path.source).match(request.path)
|
21
|
+
next unless match
|
22
|
+
|
23
|
+
nspace = r.defaults.fetch(:namespace, '')
|
24
|
+
match.captures.each_with_index { |c, i| path.gsub!(c, "{#{r.path.names[i]}}") unless c.nil? }
|
25
|
+
end
|
26
|
+
@openapi_path = { method: request.method.downcase, path: path, namespace: "#{nspace}_" }
|
27
|
+
params.merge!(openapi_path: @openapi_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def render_rest(response)
|
31
|
+
OpenAPIRest::RestRenderer.new(controller: self, response: response).render
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
module Operations
|
3
|
+
###
|
4
|
+
# Rest filter operation
|
5
|
+
#
|
6
|
+
class Filter
|
7
|
+
def initialize(query_builder)
|
8
|
+
@query_builder = query_builder
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
return if @query_builder.query.count.zero?
|
13
|
+
|
14
|
+
unlocked_params = ActiveSupport::HashWithIndifferentAccess.new(@query_builder.query)
|
15
|
+
|
16
|
+
@query_builder.api_model.model = if @query_builder.single?
|
17
|
+
@query_builder.api_model.model.find_by(unlocked_params)
|
18
|
+
else
|
19
|
+
@query_builder.api_model.model.where(unlocked_params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
module Operations
|
3
|
+
###
|
4
|
+
# Rest paginate operation
|
5
|
+
#
|
6
|
+
class Paginate
|
7
|
+
def initialize(query_builder)
|
8
|
+
@query_builder = query_builder
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
return if @query_builder.single?
|
13
|
+
|
14
|
+
@query_builder.api_model.model = @query_builder.api_model.model.limit(@query_builder.limit)
|
15
|
+
.offset(@query_builder.offset)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
module Operations
|
3
|
+
###
|
4
|
+
# Rest sort operation
|
5
|
+
#
|
6
|
+
class Sort
|
7
|
+
def initialize(query_builder)
|
8
|
+
@query_builder = query_builder
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
return if @query_builder.single? || !@query_builder.sort.present?
|
13
|
+
|
14
|
+
sorts = @query_builder.sort.split(',')
|
15
|
+
order = sorts.map do |s|
|
16
|
+
if URI.encode_www_form_component(s)[0] == '+'
|
17
|
+
"#{s[1..s.length]} ASC"
|
18
|
+
else
|
19
|
+
"#{s[1..s.length]} DESC"
|
20
|
+
end
|
21
|
+
end.join(',')
|
22
|
+
|
23
|
+
@query_builder.api_model.model = @query_builder.api_model.model.order(order)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Rest Query Builder
|
4
|
+
#
|
5
|
+
class QueryBuilder
|
6
|
+
attr_reader :query
|
7
|
+
attr_reader :sort
|
8
|
+
attr_reader :limit
|
9
|
+
attr_reader :offset
|
10
|
+
attr_reader :fields
|
11
|
+
attr_reader :api_model
|
12
|
+
attr_reader :params
|
13
|
+
attr_reader :openapi_path
|
14
|
+
|
15
|
+
def initialize(api_model, params)
|
16
|
+
@fields = params.fetch(:fields, '')
|
17
|
+
@offset = params.fetch(:offset, 0)
|
18
|
+
@limit = params.fetch(:limit, 10)
|
19
|
+
@sort = params[:sort]
|
20
|
+
@embed = params[:embed]
|
21
|
+
@query = params.fetch(:query, {})
|
22
|
+
@openapi_path = params.fetch(:openapi_path)
|
23
|
+
@single = params[:operation] == :squery
|
24
|
+
@params = params
|
25
|
+
@api_model = api_model
|
26
|
+
|
27
|
+
set_fields
|
28
|
+
|
29
|
+
unless creating?
|
30
|
+
[OpenAPIRest::Operations::Filter.new(self),
|
31
|
+
OpenAPIRest::Operations::Sort.new(self),
|
32
|
+
OpenAPIRest::Operations::Paginate.new(self)].each { |operations| operations.execute }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def response
|
37
|
+
@response ||= OpenAPIRest::QueryResponse.new(self)
|
38
|
+
@response
|
39
|
+
end
|
40
|
+
|
41
|
+
def single_result?
|
42
|
+
creating? || @single
|
43
|
+
end
|
44
|
+
alias_method :single?, :single_result?
|
45
|
+
|
46
|
+
def resource
|
47
|
+
entity.to_s.singularize
|
48
|
+
end
|
49
|
+
|
50
|
+
def entity
|
51
|
+
@api_model.type.to_s.downcase.pluralize
|
52
|
+
end
|
53
|
+
|
54
|
+
def raw_model
|
55
|
+
@api_model.model
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def creating?
|
61
|
+
@params[:operation] == :create
|
62
|
+
end
|
63
|
+
|
64
|
+
def set_fields
|
65
|
+
permitted = OpenAPIRest::ApiParameters.new(api_model: @api_model,
|
66
|
+
openapi_path: @openapi_path).response_permitted_params
|
67
|
+
@fields = fields.length > 0 ? fields.split(',').select { |s| permitted.include?(s.to_sym) } : permitted
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module OpenAPIRest
|
2
|
+
###
|
3
|
+
# Rest Query Response
|
4
|
+
#
|
5
|
+
class QueryResponse
|
6
|
+
attr_reader :query_builder
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
delegate :single?, to: :query_builder
|
10
|
+
delegate :resource, to: :query_builder
|
11
|
+
delegate :entity, to: :query_builder
|
12
|
+
delegate :fields, to: :query_builder
|
13
|
+
|
14
|
+
def initialize(query_builder)
|
15
|
+
@query_builder = query_builder
|
16
|
+
@errors = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_resource
|
20
|
+
@operation = :post
|
21
|
+
|
22
|
+
api_params = OpenAPIRest::ApiParameters.new(api_model: @query_builder.api_model,
|
23
|
+
params: @query_builder.params,
|
24
|
+
openapi_path: @query_builder.openapi_path)
|
25
|
+
unless api_params.valid?
|
26
|
+
@errors = api_params.validation_errors
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
create_params = api_params.allowed_params.merge!(@query_builder.query)
|
31
|
+
@model = @query_builder.raw_model.new(create_params)
|
32
|
+
|
33
|
+
return if @model.valid? && @model.save
|
34
|
+
|
35
|
+
build_errors
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_resource
|
39
|
+
@operation = :patch
|
40
|
+
|
41
|
+
api_params = OpenAPIRest::ApiParameters.new(api_model: @query_builder.api_model,
|
42
|
+
params: @query_builder.params,
|
43
|
+
openapi_path: @query_builder.openapi_path)
|
44
|
+
|
45
|
+
unless api_params.valid?
|
46
|
+
@errors = api_params.validation_errors
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
@model = @query_builder.raw_model
|
51
|
+
|
52
|
+
return if !@model.nil? && @model.update(api_params.allowed_params)
|
53
|
+
|
54
|
+
build_errors
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete_resource
|
58
|
+
@operation = :delete
|
59
|
+
@model = @query_builder.raw_model
|
60
|
+
return if !@model.nil? && @model.destroy
|
61
|
+
|
62
|
+
build_errors
|
63
|
+
end
|
64
|
+
|
65
|
+
def results?
|
66
|
+
if single?
|
67
|
+
!results.nil?
|
68
|
+
else
|
69
|
+
results.count > 0
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def results
|
74
|
+
@model ||= @query_builder.raw_model
|
75
|
+
end
|
76
|
+
|
77
|
+
def errors?
|
78
|
+
!@errors.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def build_errors
|
84
|
+
return if @model.nil?
|
85
|
+
|
86
|
+
@errors = @model.errors.keys.map { |k| { k => @model.errors[k].first } }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|