openapi_rest 0.0.1
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 +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
|