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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +141 -0
  4. data/Rakefile +11 -0
  5. data/bin/console +14 -0
  6. data/bin/setup +8 -0
  7. data/lib/generators/openapi_rest/install_generator.rb +11 -0
  8. data/lib/generators/openapi_rest/openapi_doc.rb +3 -0
  9. data/lib/generators/templates/api_docs.yml +162 -0
  10. data/lib/openapi_rest.rb +22 -0
  11. data/lib/openapi_rest/api_config.rb +12 -0
  12. data/lib/openapi_rest/api_doc.rb +19 -0
  13. data/lib/openapi_rest/api_doc_parser.rb +110 -0
  14. data/lib/openapi_rest/api_model.rb +36 -0
  15. data/lib/openapi_rest/api_parameters.rb +76 -0
  16. data/lib/openapi_rest/api_validator.rb +20 -0
  17. data/lib/openapi_rest/extension.rb +35 -0
  18. data/lib/openapi_rest/operations/filter.rb +24 -0
  19. data/lib/openapi_rest/operations/paginate.rb +19 -0
  20. data/lib/openapi_rest/operations/sort.rb +27 -0
  21. data/lib/openapi_rest/query_builder.rb +70 -0
  22. data/lib/openapi_rest/query_response.rb +89 -0
  23. data/lib/openapi_rest/railtie.rb +9 -0
  24. data/lib/openapi_rest/rest_renderer.rb +75 -0
  25. data/lib/openapi_rest/validators/format.rb +24 -0
  26. data/lib/openapi_rest/validators/pattern.rb +21 -0
  27. data/lib/openapi_rest/version.rb +3 -0
  28. data/test/openapi_rest/api_config_test.rb +9 -0
  29. data/test/openapi_rest/api_doc_parser_test.rb +71 -0
  30. data/test/openapi_rest/api_doc_test.rb +15 -0
  31. data/test/openapi_rest/api_model_test.rb +49 -0
  32. data/test/openapi_rest/api_parameters_test.rb +57 -0
  33. data/test/openapi_rest/api_validator_test.rb +23 -0
  34. data/test/openapi_rest/query_builder_test.rb +32 -0
  35. data/test/openapi_rest/query_response_test.rb +129 -0
  36. data/test/spec_helper.rb +8 -0
  37. data/test/support/data.rb +3 -0
  38. data/test/support/models.rb +3 -0
  39. data/test/support/schema.rb +13 -0
  40. 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