openapi_rest 0.0.1

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