open-api 0.8.0 → 0.8.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0d0cd8c8411ef75344e1c82168450f2380e44626
4
- data.tar.gz: 38a76fa2758c2d2c28106c6639b6c01e6ebc5f5a
3
+ metadata.gz: 4058f027c10a1877a4d58919f0f919f282c443a5
4
+ data.tar.gz: 1cca862076eb32844b8dee56d2757dbd545efcd7
5
5
  SHA512:
6
- metadata.gz: da87e21f7639792c895297f901274fb1560d88cc57bf3a171baaf28576718b77becfb98dfce9094851190d47357971220d435ec750799ad5352483e181f54cca
7
- data.tar.gz: 458bc117fd7c642d422fd518ede8697b529aae571abdccb402040d2f3de02ac36c59f5259dd89a6b15ad47dade93751ae214250d93a1b973d6178856297312b9
6
+ metadata.gz: db59e78681bd7d0826fe61cf6623adf3675494f0e1a743f74a1edf8da095dd50f04cf5eec5ae373238006dc7ab1ce7d67451c9ae0dab4cf2d26870853985e60d
7
+ data.tar.gz: 60c02fa2366b4db2fd6412dd928e264e34e4b86a6b128ca2cfe0ac2d7f114ab6dc5a716c6fbd92865e91b8de8cfdc34db15da79bcc4fad833a14f4ba158a6fc0
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # open-api
2
+
3
+ **A flexible, inline [OpenAPI](https://github.com/OAI/OpenAPI-Specification) documentation solution
4
+ for your Rails-based REST API.**
5
+
6
+ [OpenAPI](https://github.com/OAI/OpenAPI-Specification) (formerly Swagger) is a popular,
7
+ JSON-based, language-agnostic standard for documenting a REST API. It's quickly becoming the
8
+ industry-leading appraoch for describing REST API's of all shapes and sizes.
9
+
10
+ However, maintaining your own lengthy, stand-alone JSON documentation alongside your Rails API
11
+ source code is tedious and error-prone process to say the least. Here's why using the open-api gem
12
+ is better:
13
+
14
+ + The open-api gem merges documentation details you provide with API metadata supplied by the
15
+ Rails framework itself, reducing your documentation effort and helping to maintain the accuracy
16
+ of your documentation over time.
17
+ + Your API documentation details live inline right alongside your API source code. As your API
18
+ changes, locating and updating documentation affected by those changes becomes a far easier task.
19
+ + Metadata inheritance and intelligent merging rules miminize the need to document anything more
20
+ than once, further reducing the development and maintenance burden associated with your API
21
+ documentation.
22
+ + Metadata that's not directly interpreted by open-api is generally passed through to the output
23
+ JSON intact. As the OpenAPI standard evolves, you won't be limited to using OpenAPI features the
24
+ gem was explicitly written to manage.
25
+
26
+ ## Table of Contents
27
+
28
+ ## Installation
29
+
30
+ Put this in your Gemfile:
31
+
32
+ ``` ruby
33
+ gem 'open-api'
34
+ ```
35
+ ## Configuration
36
+
37
+ Configuration for the open-api gem is performed using an `open_api.rb` initializer in your
38
+ `config/initializers` subdirectory. A sample initializer follows:
39
+
40
+ ``` ruby
41
+ OpenApi.configure do |config|
42
+
43
+ # Default base path(s), used to scan Rails routes for API endpoints.
44
+ config.base_paths = ['/widget-api/v1']
45
+
46
+ # General information about your API.
47
+ config.info = {
48
+ title: 'Acme Widget API',
49
+ description: "Documentation of the Acme's Widget API service",
50
+ version: '1.0.0',
51
+ terms_of_service: 'https://www.acme.com/widget-api/terms_of_service',
52
+
53
+ contact: {
54
+ name: 'Acme Corporation API Team',
55
+ url: 'http://www.acme.com/widget-api',
56
+ email: 'widget-api-support@acme.com'
57
+ },
58
+
59
+ license: {
60
+ name: 'Apache 2.0',
61
+ url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
62
+ }
63
+ }
64
+
65
+ # Default output file path for your generated Open API JSON document.
66
+ config.output_file_path = Rails.root.join('apidoc', 'api-docs.json')
67
+ end
68
+ ```
69
+
70
+ For details regarding content you may include in the "info" section of your API documentation, see
71
+ the[OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject).
72
+
73
+
74
+ ## Describing Endpoints
75
+
76
+ ### Controller-Level Metadata
77
+
78
+ Controller-level metadata is Open API documentation metadata common to all endpoints for a
79
+ controller, as well as any endpoints associated with that controller's subclasses. Below is an
80
+ example of how characteristics common to all endpoints in an API might be defined in a base API
81
+ controller:
82
+ ``` ruby
83
+ class BaseApiController < ActionController::Base
84
+
85
+ # Include once in your base API controller class
86
+ include OpenApi::Controller
87
+
88
+ open_api_controller \
89
+ query_string: {
90
+ access_token: {
91
+ type: :string,
92
+ description: 'OAuth 2 access token query parameter',
93
+ required: false
94
+ }
95
+ },
96
+ headers: {
97
+ 'X-Access-Token' => {
98
+ type: :string,
99
+ description: 'OAuth 2 access token HTTP header',
100
+ required: false
101
+ }
102
+ },
103
+ responses: {
104
+ 200 => { description: 'Successful' },
105
+ 401 => { description: 'Invalid request' },
106
+ 403 => { description: 'Not authorized' }
107
+ }
108
+ ```
109
+
110
+ Another common use of `api_controller` is to define a tag for all endpoints associated with a
111
+ specific controller:
112
+ ``` ruby
113
+ class WidgetController < BaseApiController
114
+
115
+ open_api_controller \
116
+ tag: {
117
+ name: 'Widgets',
118
+ description: 'Manage the widgets associated with your user account'
119
+ }
120
+ ```
121
+ Note that, in the example above, any documentation metadata specified for `BaseApiController` is
122
+ inherited for endpoints defined in `WidgetController`. If the same metadata key is defined for both
123
+ controllers, the child controller's metadata will override the superclass' for that key.
124
+
125
+ Note also that the process of merging metadata in a class hierarchy isn't as simple as doing a
126
+ top-level merge or recursive merge for metadata belonging to the classes in that hierarchy. The
127
+ merge process can vary depending on the sort of metadata being merged. For example, when two query
128
+ string parameter lists are merged amongst classes in a hierarchy, the query string entries will be
129
+ merged recursively. This might, for example, allow a description to be amended in a child
130
+ controller to a query string parameter defined in a base controller. For other metadata, the
131
+ collection value of a parent controller might be entirely replaced.
132
+
133
+
134
+ ## Describing Objects
135
+ ## Generating Documentation
136
+
137
+ Generate OpenApi (Swagger) JSON by running the following:
138
+
139
+ rake open_api:docs
140
+
141
+ Optionally, you may specify the base path and output file:
142
+
143
+ rake open_api:docs[/api/v1,/home/myhome/api-v1.json]
@@ -0,0 +1,9 @@
1
+ module OpenApi
2
+ module Controller
3
+ def self.included(base)
4
+ base.send(:include, OpenApi::Endpoints::Controller)
5
+ base.send(:include, OpenApi::Objects::Controller)
6
+ base.send(:include, OpenApi::Tags::Controller)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,173 @@
1
+ module OpenApi
2
+ class Endpoints
3
+ class << self
4
+ METADATA_MERGE = {
5
+ tags: (lambda do |tags, merge_tags|
6
+ tags ||= {}
7
+ return tags if merge_tags.nil?
8
+ fail 'Expected tags as an Array!' unless merge_tags.is_a?(Array)
9
+ tags + merge_tags
10
+ end),
11
+ headers: (lambda do |headers, merge_headers, opts|
12
+ OpenApi::Utils.verify_and_merge_hash(headers, merge_headers, 'header parameters', opts)
13
+ end),
14
+ path_params: (lambda do |path_params, merge_path_params, opts|
15
+ OpenApi::Utils.verify_and_merge_hash(path_params, merge_path_params, 'path parameters',
16
+ opts)
17
+ end),
18
+ query_string: (lambda do |query_string, merge_query_string, opts|
19
+ OpenApi::Utils.verify_and_merge_hash(query_string, merge_query_string,
20
+ 'query string parameters', opts)
21
+ end),
22
+ form_data: (lambda do |form_data, merge_form_data, opts|
23
+ OpenApi::Utils.verify_and_merge_hash(form_data, merge_form_data, 'form data parameters',
24
+ opts)
25
+ end),
26
+ body: (lambda do |body_data, merge_body_data, opts|
27
+ OpenApi::Utils.verify_and_merge_hash(body_data, merge_body_data, 'body', opts)
28
+ end),
29
+ responses: (lambda do |responses, merge_responses, opts|
30
+ merge_responses = Hash[(merge_responses.map do |key, hash|
31
+ fail "Invalid response code #{key}" if key.to_s != key.to_i.to_s
32
+ [key.to_i, hash]
33
+ end)]
34
+ OpenApi::Utils.verify_and_merge_hash(responses, merge_responses, 'responses', opts)
35
+ end)
36
+ }
37
+
38
+ def merge_metadata(metadata, merge_metadata, opts = {})
39
+ if (body_value = merge_metadata[:body]).respond_to?(:to_sym)
40
+ merge_metadata = merge_metadata.merge(body: { schema: { :'$ref' => body_value.to_sym } })
41
+ end
42
+ if merge_metadata.include?(:children)
43
+ merge_metadata = merge_metadata.reject { |k, _v| k == :children }
44
+ end
45
+ OpenApi::Utils.merge_hash(metadata, merge_metadata, opts.merge(merge_by: METADATA_MERGE))
46
+ end
47
+
48
+ def relative_path(path, base_path)
49
+ return path if path.blank? || base_path.blank?
50
+ relative_path = path[base_path.length..-1]
51
+ relative_path = "/#{relative_path}" unless relative_path.starts_with?('/')
52
+ relative_path
53
+ end
54
+
55
+ def verb_key(route_wrapper)
56
+ route_wrapper.verb.to_s.downcase
57
+ end
58
+
59
+ def find_matching_routes(base_path, opts = {})
60
+ path_filter = opts[:path_filter]
61
+ if path_filter.is_a?(String) && !path_filter.starts_with?('/')
62
+ path_filter = "/#{path_filter}"
63
+ end
64
+ matching_routes = []
65
+ Rails.application.routes.routes.each do |route|
66
+ route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
67
+ if (path = route_wrapper.path.to_s).starts_with?(base_path)
68
+ next unless check_path_filter(route, relative_path(path, base_path), path_filter, opts)
69
+ matching_routes << route_wrapper
70
+ next
71
+ end
72
+ end
73
+ matching_routes
74
+ end
75
+
76
+ def check_path_filter(route, rel_path, path_filter, opts = {})
77
+ return true unless path_filter.present?
78
+ return rel_path == path_filter if path_filter.is_a?(String)
79
+ return rel_path =~ path_filter if path_filter.is_a?(Regexp)
80
+ return path_filter.include?(rel_path) if path_filter.is_a?(Array)
81
+ if path_filter.respond_to?(:call) && path_filter.respond_to?(:parameters)
82
+ route_opts = opts.merge(route: route, controller: route.defaults[:controller],
83
+ action: route.defaults[:action])
84
+ rslt = path_filter.send(*([:call, rel_path, route_opts][0..path_filter.parameters.size]))
85
+ return rslt ? true : false
86
+ end
87
+ false
88
+ end
89
+
90
+ def build_parameter_metadata(endpoint_metadata)
91
+ parameters = {}
92
+ parameters = (parameters.is_a?(Array) ? parameters : []) +
93
+ param_array(endpoint_metadata.delete(:headers), :header) +
94
+ param_array(endpoint_metadata.delete(:path_params), :path) +
95
+ param_array(endpoint_metadata.delete(:query_string), :query) +
96
+ param_array(endpoint_metadata.delete(:form_data), :form_data)
97
+ if (body_param = endpoint_metadata.delete(:body)).is_a?(Hash)
98
+ parameters += param_array({ body: body_param }, :body)
99
+ end
100
+ return unless parameters.present?
101
+ endpoint_metadata[:parameters] = OpenApi::Utils.camelize_metadata(parameters, end_depth: 3)
102
+ end
103
+
104
+ private
105
+
106
+ def param_array(hash, param_in)
107
+ return [] if hash.nil?
108
+ fail "Expected Hash for parameter type '#{param_in}'!" unless hash.is_a?(Hash)
109
+ hash.map do |key, value|
110
+ { name: key, in: OpenApi::Utils.camelize_key(param_in) }.merge(
111
+ value.reject { |k, _v| [:name, :in].include?(k) })
112
+ end
113
+ end
114
+ end
115
+
116
+ module Controller
117
+ module ClassMethods
118
+ def open_api_path(path, metadata = nil)
119
+ @open_api_path_metadata ||= {} if metadata.present?
120
+ OpenApi::Utils.metadata_by_string_or_regexp(@open_api_path_metadata, path, metadata,
121
+ arg_name: 'path')
122
+ end
123
+
124
+ def open_api_path_param(path_param, param_metadata)
125
+ regexp = %r{(\A|\/)\:#{path_param}(\Z|\/)|\(\.\:#{path_param}\)}
126
+ open_api_path regexp, path_params: {
127
+ path_param.to_s => { type: :integer, required: true }.merge(param_metadata)
128
+ }
129
+ end
130
+
131
+ def open_api_controller(metadata = nil)
132
+ return (@open_api_controller_metadata || {}).deep_dup if metadata.blank?
133
+ fail 'Expected Hash argument for open_api_controller()!' unless metadata.is_a?(Hash)
134
+ OpenApi::Endpoints.merge_metadata(@open_api_controller_metadata ||= {}, metadata)
135
+ nil
136
+ end
137
+
138
+ def open_api_action(action, metadata = nil)
139
+ @open_api_action_metadata ||= {} if metadata.present?
140
+ OpenApi::Utils.metadata_by_string_or_regexp(@open_api_action_metadata, action, metadata,
141
+ arg_name: 'action')
142
+ end
143
+
144
+ def open_api_endpoint_metadata(action, path, opts = {})
145
+ path = OpenApi::Endpoints.relative_path(path, opts[:base_path])
146
+ controller_class_hierarchy = OpenApi::Utils.controller_class_hierarchy(self)
147
+ aggr_controller_metadata = {}
148
+ controller_class_hierarchy.each do |controller_class|
149
+ OpenApi::Endpoints.merge_metadata(aggr_controller_metadata,
150
+ controller_class.send(:open_api_controller), opts)
151
+ end
152
+ aggr_path_metadata = {}
153
+ controller_class_hierarchy.each do |controller_class|
154
+ OpenApi::Endpoints.merge_metadata(aggr_path_metadata,
155
+ controller_class.send(:open_api_path, path), opts)
156
+ end
157
+ aggr_action_metadata = {}
158
+ controller_class_hierarchy.each do |controller_class|
159
+ OpenApi::Endpoints.merge_metadata(aggr_action_metadata,
160
+ controller_class.send(:open_api_action, action), opts)
161
+ end
162
+ OpenApi::Endpoints.merge_metadata(aggr_controller_metadata,
163
+ OpenApi::Endpoints.merge_metadata(aggr_action_metadata, aggr_path_metadata,
164
+ opts), opts)
165
+ end
166
+ end
167
+
168
+ def self.included(base)
169
+ base.extend(ClassMethods)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,153 @@
1
+ # rubocop:disable Rails/Output
2
+ module OpenApi
3
+ class Generator
4
+ HIDDEN_ROOT_KEYS = [:output_file_path, :base_paths]
5
+ class << self
6
+ def build(opts = {})
7
+ base_paths = find_base_paths(opts)
8
+
9
+ doc = OpenApi.global_metadata.reject { |k, _v| HIDDEN_ROOT_KEYS.include?(k.to_sym) }
10
+ doc[:info] = OpenApi::Utils.camelize_metadata(doc[:info]) if doc[:info].is_a?(Hash)
11
+
12
+ tags, paths, definitions = build_endpoint_content(base_paths, opts)
13
+ doc[:tags] = OpenApi::Utils.camelize_metadata(tags.values) if tags.present?
14
+ doc[:paths] = OpenApi::Utils.camelize_metadata(paths, start_depth: 2, end_depth: 4)
15
+ doc[:definitions] = OpenApi::Utils.camelize_metadata(definitions, start_depth: 2,
16
+ end_depth: 3)
17
+
18
+ doc = OpenApi::Utils.camelize_metadata(doc, end_depth: 2)
19
+ doc[:swagger] = doc[:swagger].to_s if doc.include?(:swagger)
20
+
21
+ doc
22
+ end
23
+
24
+ def write(opts = {})
25
+ output_file_path = opts[:output_file_path] || OpenApi.global_metadata[:output_file_path]
26
+ unless output_file_path.respond_to?(:to_s) && output_file_path.to_s.present?
27
+ fail 'Missing output file path; Must be passed as output_file_path option, or ' \
28
+ 'output_file_path must be configured in the OpenApi initializer ' \
29
+ '(config/initializers/open_api.rb)'
30
+ end
31
+
32
+ doc = nil
33
+ File.open(output_file_path.to_s, 'w') do |file|
34
+ file.write JSON.pretty_generate(doc = build(opts))
35
+ end
36
+
37
+ doc
38
+ end
39
+
40
+ def log_message(level, message, opts = {})
41
+ unless [:debug, :info, :warn, :error, :fatal].include?(level)
42
+ fail "Invalid message level: #{level}"
43
+ end
44
+ if opts[:stdout]
45
+ puts "[#{level}] #{message}"
46
+ else
47
+ Rails.logger.send(level, message.to_s)
48
+ end
49
+ end
50
+
51
+ def build_endpoint_content(base_paths, opts = {})
52
+ paths = {}
53
+ tags = {}
54
+ definitions = {}
55
+ common_base_path = find_common_base_path(base_paths)
56
+ global_opts = opts.merge(base_path: common_base_path)
57
+ base_paths.each do |base_path, base_path_opts|
58
+ opts = global_opts.merge(base_path_opts)
59
+ OpenApi::Endpoints.find_matching_routes(base_path, opts).each do |route_wrapper|
60
+ controller_name = (route_wrapper.controller).split('/').map(&:camelize).join('::') +
61
+ 'Controller'
62
+ controller = controller_name.constantize
63
+ if controller.nil?
64
+ log_message(:warn, "Can't resolve controller: #{route_wrapper.controller}", opts)
65
+ next
66
+ end
67
+ next unless controller.respond_to?(:open_api_endpoint_metadata)
68
+ endpoint_metadata = controller.open_api_endpoint_metadata(route_wrapper.action,
69
+ route_wrapper.path, opts.merge(base_path: base_path))
70
+ next if endpoint_metadata[:hidden]
71
+ path = add_path(route_wrapper, paths, common_base_path, opts)
72
+ next if path.nil?
73
+ OpenApi::Endpoints.build_parameter_metadata(endpoint_metadata)
74
+ endpoint_metadata = OpenApi::Objects.resolve_refs(endpoint_metadata, definitions,
75
+ controller, opts)
76
+ endpoint_metadata = OpenApi::Tags.resolve_refs(endpoint_metadata, tags, controller,
77
+ opts)
78
+ path[OpenApi::Endpoints.verb_key(route_wrapper)] = endpoint_metadata
79
+ end
80
+ end
81
+ [tags, paths, definitions]
82
+ end
83
+
84
+ def add_path(route_wrapper, paths, common_base_path, opts = {})
85
+ relative_path = OpenApi::Endpoints.relative_path(route_wrapper.path.to_s, common_base_path)
86
+ route_wrapper.parts.each do |path_param|
87
+ relative_path = relative_path
88
+ .gsub(%r{(\A|\/)\:(#{path_param})(\Z|\/)}, '\1{\2}\3')
89
+ .gsub(/\(\.\:#{path_param}\)/, ".{#{path_param}}")
90
+ end
91
+ path = (paths[relative_path] ||= {})
92
+ verb_key = OpenApi::Endpoints.verb_key(route_wrapper)
93
+ if path.include?(verb_key)
94
+ base_message = "Warning: Multiple OpenApi::Endpoints match #{route_wrapper.verb} " \
95
+ "#{relative_path} ... skipping entry for route"
96
+ if route_wrapper.name.present?
97
+ log_message(:warn, "#{base_message} '#{route_wrapper.name}'", opts)
98
+ else
99
+ log_message(:warn, "#{base_message} #{route_wrapper.verb} #{route_wrapper.path}", opts)
100
+ end
101
+ return nil
102
+ end
103
+ path
104
+ end
105
+
106
+ def find_base_paths(opts = {})
107
+ base_paths = opts[:base_paths] || OpenApi.global_metadata[:base_paths]
108
+ if base_paths.is_a?(Array)
109
+ base_paths = base_paths.map(&:to_s).reject(&:blank?).uniq
110
+ base_paths = Hash[(base_paths.map do |base_path|
111
+ [base_path.starts_with?('/') ? base_path : "/#{base_path}", {}]
112
+ end)]
113
+ elsif base_paths.is_a?(Hash)
114
+ base_paths = Hash[(base_paths.map do |base_path, api_opts|
115
+ fail "Expected options hash for base path '#{base_path}'" unless api_opts.is_a?(Hash)
116
+ [base_path.starts_with?('/') ? base_path : "/#{base_path}", api_opts]
117
+ end)]
118
+ else
119
+ fail "Invalid value for 'base_paths': Expected Hash or Array"
120
+ end
121
+ if base_paths.blank?
122
+ fail 'Missing API base paths; Must be passed as base_paths option, or base_paths must ' \
123
+ 'be configured in the OpenApi initializer (config/initializers/open_api.rb)'
124
+ end
125
+ base_paths
126
+ end
127
+
128
+ def find_common_base_path(base_paths)
129
+ return nil if base_paths.blank?
130
+ split_paths = base_paths.map do |base_path|
131
+ base_path.split('/').reject(&:blank?)
132
+ end
133
+ path_count = split_paths.length
134
+ first_path = split_paths[0]
135
+ return "/#{first_path.join('/')}" if path_count == 1
136
+ common_elems = 0
137
+ while common_elems < first_path.length
138
+ path_elem_idx = 0
139
+ while path_elem_idx < path_count - 1
140
+ cmp_path = split_paths[path_elem_idx + 1]
141
+ break if cmp_path.length <= common_elems
142
+ break if cmp_path[common_elems] != first_path[common_elems]
143
+ path_elem_idx += 1
144
+ end
145
+ break if path_elem_idx < path_count - 1
146
+ common_elems += 1
147
+ end
148
+ return '/' if common_elems == 0
149
+ "/#{first_path[0..(common_elems - 1)].join('/')}"
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,150 @@
1
+ module OpenApi
2
+ module Objects
3
+ class << self
4
+ METADATA_MERGE = {
5
+ properties: (lambda do |properties, merge_properties, opts|
6
+ OpenApi::Utils.verify_and_merge_hash(properties, merge_properties, 'properties',
7
+ opts.merge(recursive_merge: true))
8
+ end)
9
+ }
10
+
11
+ def merge_metadata(metadata, merge_metadata, opts = {})
12
+ OpenApi::Utils.merge_hash(metadata, merge_metadata, opts.merge(merge_by: METADATA_MERGE))
13
+ end
14
+
15
+ def resolve_refs(metadata, definitions, controller, opts = {})
16
+ resolve_proc = -> (object_name) { controller.open_api_object_metadata(object_name) }
17
+ if metadata.is_a?(Hash)
18
+ Hash[(metadata.map do |key, value|
19
+ value = resolve_refs(value, definitions, controller, opts)
20
+ if ['schema', 'items', '$ref'].include?(key.to_s) && value.respond_to?(:to_sym) &&
21
+ !(object = resolve_ref(value.to_sym, resolve_proc)).nil?
22
+ fail 'Expected Hash for definitions!' unless definitions.is_a?(Hash)
23
+ object = resolve_refs(object, definitions, controller, opts)
24
+ add_definition(definitions, value.to_sym, object)
25
+ next [:'$ref', "#/definitions/#{value}"] if key.to_s == '$ref'
26
+ next [key.to_sym, { :'$ref' => "#/definitions/#{value}" }]
27
+ end
28
+ [key, value]
29
+ end)]
30
+ elsif metadata.is_a?(Array)
31
+ metadata.map do |elem|
32
+ resolve_refs(elem, definitions, controller, opts)
33
+ end
34
+ else
35
+ metadata
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def resolve_ref(key, resolve_proc, opts = {})
42
+ unless resolve_proc.respond_to?(:call) &&
43
+ resolve_proc.respond_to?(:parameters)
44
+ fail 'Expected proc/lambda for resolve_proc!'
45
+ end
46
+ proc_param_count = resolve_proc.parameters.size
47
+ fail 'Expected 1+ parameters (object name) for resolve_proc!' if proc_param_count < 1
48
+ object = resolve_proc.send(*([:call, key, opts][0..proc_param_count]))
49
+ return nil if object.nil?
50
+ fail 'Expected hash result from resolve_proc!' unless object.is_a?(Hash)
51
+ object
52
+ end
53
+
54
+ def add_definition(definitions, key, object)
55
+ return if object.nil?
56
+ json = object.to_json
57
+ retry_index = 0
58
+ loop do
59
+ if definitions.include?(key)
60
+ break if definitions[key].to_json == json
61
+ retry_index += 1
62
+ key = "#{key}#{retry_index}".to_sym
63
+ else
64
+ definitions[key] = object
65
+ break
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ module Controller
72
+ module ClassMethods
73
+ def open_api_objects(metadata = nil)
74
+ return (@open_api_objects_metadata || {}).deep_dup if metadata.blank?
75
+ fail 'Expected Hash argument for open_api_objects()!' unless metadata.is_a?(Hash)
76
+ metadata.each do |object_key, object_metadata|
77
+ open_api_object(object_key, object_metadata)
78
+ end
79
+ nil
80
+ end
81
+
82
+ def open_api_object(object_key, metadata = nil)
83
+ fail 'Valid object argument required!' unless object_key.respond_to?(:to_sym)
84
+ return (@open_api_objects_metadata || {})[object_key.to_sym].deep_dup if metadata.blank?
85
+ fail 'Expected Hash argument for open_api_object()!' unless metadata.is_a?(Hash)
86
+ metadata = expand_nested_object_metadata(metadata)
87
+ object_metadata = ((@open_api_objects_metadata ||= {})[object_key.to_sym] ||= {})
88
+ OpenApi::Objects.merge_metadata(object_metadata, metadata)
89
+ nil
90
+ end
91
+
92
+ def expand_nested_object_metadata(metadata)
93
+ unless metadata[:type].respond_to?(:to_sym) && metadata[:type].to_sym == :object
94
+ metadata = { type: :object, properties: metadata }
95
+ end
96
+ required_attrs = (metadata[:required] || []).map(&:to_sym)
97
+ if (properties = metadata[:properties]).is_a?(Hash) && properties.present?
98
+ metadata = metadata.dup
99
+ properties = Hash[(properties.map do |name, property|
100
+ property = expand_nested_object_property(name, property, required_attrs)
101
+ [name, property]
102
+ end)]
103
+ metadata[:properties] = properties
104
+ end
105
+ metadata[:required] = required_attrs.uniq if required_attrs.present?
106
+ metadata
107
+ end
108
+
109
+ def expand_nested_object_property(name, property, required_attrs)
110
+ if property.is_a?(Hash)
111
+ required = property[:required]
112
+ if required.nil?
113
+ required_attrs << name.to_sym # Presume required if required option not spec'd
114
+ elsif [TrueClass, FalseClass].include?(required.class)
115
+ required_attrs << name.to_sym if property.delete(:required)
116
+ end
117
+ unless property.blank? || property[:type].respond_to?(:to_sym)
118
+ property = expand_nested_object_metadata(property)
119
+ end
120
+ else
121
+ api_type, api_format = OpenApi::Utils.open_api_type_and_format(property)
122
+ if api_type.nil?
123
+ property = { type: :object, '$ref' => property }
124
+ elsif api_format.present?
125
+ property = { type: api_type, format: api_format }
126
+ else
127
+ property = { type: api_type }
128
+ end
129
+ required_attrs << name.to_sym # Presume required if required option not spec'd
130
+ end
131
+ property
132
+ end
133
+
134
+ def open_api_object_metadata(object_key, opts = {})
135
+ controller_class_hierarchy = OpenApi::Utils.controller_class_hierarchy(self)
136
+ aggr_object_metadata = {}
137
+ controller_class_hierarchy.each do |controller_class|
138
+ OpenApi::Objects.merge_metadata(aggr_object_metadata,
139
+ controller_class.send(:open_api_object, object_key), opts)
140
+ end
141
+ aggr_object_metadata
142
+ end
143
+ end
144
+
145
+ def self.included(base)
146
+ base.extend(ClassMethods)
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,149 @@
1
+ module OpenApi
2
+ class Tags
3
+ class << self
4
+ def merge_metadata(metadata, merge_metadata, opts = {})
5
+ OpenApi::Utils.merge_hash(metadata, merge_metadata, opts.merge(recursive_merge: true))
6
+ end
7
+
8
+ def resolve_refs(metadata, tags, controller, opts = {})
9
+ opts = opts.symbolize_keys
10
+ opts[:define_proc] ||=
11
+ -> (tag_name, tag_metadata) { controller.open_api_tag(tag_name, tag_metadata) }
12
+ opts[:resolve_proc] ||=
13
+ -> (tag_name) { controller.open_api_tag_metadata(tag_name) }
14
+ if metadata.is_a?(Hash)
15
+ resolve_hash_refs(metadata, tags, controller, opts)
16
+ elsif metadata.is_a?(Array)
17
+ metadata.map do |elem|
18
+ resolve_refs(elem, tags, controller, opts)
19
+ end
20
+ else
21
+ metadata
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_hash_refs(hash, tags, controller, opts)
28
+ Hash[(hash.map do |key, value|
29
+ next [key, value] unless %w(tag tags).include?(key.to_s)
30
+ if key.to_s == 'tag'
31
+ values = [value]
32
+ else
33
+ next [key, value] unless value.is_a?(Array)
34
+ values = value
35
+ end
36
+ values = resolve_tag_values(values, tags, controller, opts)
37
+ next values.nil? ? [key, values] : [:tags, values]
38
+ end)]
39
+ end
40
+
41
+ def resolve_tag_values(values, tags, controller, opts = {})
42
+ define_proc = opts[:define_proc] ||
43
+ -> (tag_name, metadata) { controller.open_api_tag(tag_name, metadata) }
44
+ resolve_proc = opts[:resolve_proc] ||
45
+ -> (tag_name) { controller.open_api_tag_metadata(tag_name) }
46
+ values = (values.map do |value|
47
+ if value.is_a?(Hash)
48
+ next nil unless value[:name].respond_to?(:to_sym)
49
+ name = value[:name].to_s
50
+ value = value.merge(name: name)
51
+ metadata = define_ref(name, value, define_proc, opts)
52
+ add_tag(tags, name, metadata)
53
+ name
54
+ else
55
+ value.respond_to?(:to_sym) ? value.to_s : nil
56
+ end
57
+ end).compact
58
+ return nil if values.empty?
59
+ values = (values.map do |tag|
60
+ next nil unless tag.respond_to?(:to_sym)
61
+ name = tag.to_s
62
+ metadata = resolve_ref(name, resolve_proc, opts)
63
+ fail 'Expected Hash for metadata!' unless metadata.is_a?(Hash)
64
+ add_tag(tags, name, metadata)
65
+ name
66
+ end).compact
67
+ values
68
+ end
69
+
70
+ def resolve_ref(key, resolve_proc, opts = {})
71
+ unless resolve_proc.respond_to?(:call) &&
72
+ resolve_proc.respond_to?(:parameters)
73
+ fail 'Expected proc/lambda for resolve_proc!'
74
+ end
75
+ proc_param_count = resolve_proc.parameters.size
76
+ fail 'Expected 1+ parameters (tag) for resolve_proc!' if proc_param_count < 1
77
+ tag = resolve_proc.send(*([:call, key, opts][0..proc_param_count]))
78
+ return nil if tag.nil?
79
+ fail 'Expected hash result from resolve_proc!' unless tag.is_a?(Hash)
80
+ tag
81
+ end
82
+
83
+ def define_ref(key, metadata, define_proc, opts = {})
84
+ unless define_proc.respond_to?(:call) &&
85
+ define_proc.respond_to?(:parameters)
86
+ fail 'Expected proc/lambda for define_proc!'
87
+ end
88
+ proc_param_count = define_proc.parameters.size
89
+ fail 'Expected 2+ parameters (tag, metadata) for define_proc!' if proc_param_count < 1
90
+ tag = define_proc.send(*([:call, key, metadata, opts][0..proc_param_count]))
91
+ return nil if tag.nil?
92
+ fail 'Expected hash result from define_proc!' unless tag.is_a?(Hash)
93
+ tag
94
+ end
95
+
96
+ def add_tag(tags, tag, metadata)
97
+ return if metadata.nil?
98
+ json = metadata.to_json
99
+ retry_index = 0
100
+ loop do
101
+ if tags.include?(tag)
102
+ break if tags[tag].to_json == json
103
+ retry_index += 1
104
+ tag = "#{tag} (#{retry_index})".to_s
105
+ else
106
+ tags[tag] = metadata
107
+ break
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ module Controller
114
+ module ClassMethods
115
+ def open_api_tags(metadata = nil)
116
+ return (@open_api_tags_metadata || {}).deep_dup if metadata.blank?
117
+ fail 'Expected Hash argument for open_api_tags()!' unless metadata.is_a?(Hash)
118
+ metadata.each { |tag, tag_metadata| open_api_tag(tag, tag_metadata) }
119
+ (@open_api_tags_metadata || {})
120
+ nil
121
+ end
122
+
123
+ def open_api_tag(tag, metadata = nil)
124
+ fail 'Valid tag argument required!' unless tag.respond_to?(:to_s)
125
+ return (@open_api_tags_metadata || {})[tag.to_s].deep_dup if metadata.blank?
126
+ fail 'Expected Hash argument for open_api_tag()!' unless metadata.is_a?(Hash)
127
+ metadata = { name: tag.to_s }.merge(metadata) unless metadata.include?(:name)
128
+ tag_metadata = ((@open_api_tags_metadata ||= {})[tag.to_s] ||= {})
129
+ OpenApi::Tags.merge_metadata(tag_metadata, metadata)
130
+ nil
131
+ end
132
+
133
+ def open_api_tag_metadata(tag, opts = {})
134
+ controller_class_hierarchy = OpenApi::Utils.controller_class_hierarchy(self)
135
+ aggr_metadata = {}
136
+ controller_class_hierarchy.each do |controller_class|
137
+ OpenApi::Tags.merge_metadata(aggr_metadata,
138
+ controller_class.send(:open_api_tag, tag), opts)
139
+ end
140
+ aggr_metadata
141
+ end
142
+ end
143
+
144
+ def self.included(base)
145
+ base.extend(ClassMethods)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,131 @@
1
+ module OpenApi
2
+ class Utils
3
+ class << self
4
+ def controller_class_hierarchy(controller_class)
5
+ controller_class_hierarchy = [controller_class]
6
+ loop do
7
+ controller_class = controller_class.superclass
8
+ break if controller_class.nil? || !controller_class.respond_to?(:open_api_controller)
9
+ controller_class_hierarchy << controller_class
10
+ end
11
+ controller_class_hierarchy
12
+ end
13
+
14
+ def verify_and_merge_hash(hash, merge_hash, hash_desc, opts = {})
15
+ return (hash || {}) if merge_hash.nil?
16
+ fail "Expected #{hash_desc} in the form of a Hash!" unless merge_hash.is_a?(Hash)
17
+ merge_hash(hash, merge_hash, opts)
18
+ end
19
+
20
+ def merge_hash(hash, merge_hash, opts = {})
21
+ hash ||= {}
22
+ return hash if merge_hash.nil?
23
+ fail 'Expected Hash!' unless merge_hash.is_a?(Hash)
24
+ merge_hash.each do |key, value|
25
+ if hash.include?(key)
26
+ merge_hash_entry(hash, key, value, opts)
27
+ elsif !value.nil?
28
+ hash[key] = value
29
+ end
30
+ end
31
+ hash
32
+ end
33
+
34
+ def camelize_metadata(metadata, opts = {})
35
+ if (start_depth = opts[:start_depth].to_i) > 0
36
+ start_depth -= 1
37
+ if start_depth > 0
38
+ opts = opts.merge(start_depth: start_depth)
39
+ else
40
+ (opts = opts.dup).delete(:start_depth)
41
+ end
42
+ end
43
+ if (end_depth = opts[:end_depth]).present?
44
+ end_depth -= 1
45
+ return metadata if end_depth <= 0
46
+ opts = opts.merge(end_depth: end_depth)
47
+ end
48
+ if metadata.is_a?(Hash)
49
+ Hash[(metadata.map do |k, v|
50
+ [start_depth > 0 ? k : camelize_key(k), camelize_metadata(v, opts)]
51
+ end)]
52
+ elsif metadata.is_a?(Array)
53
+ metadata.map { |v| camelize_metadata(v, opts) }
54
+ else
55
+ metadata
56
+ end
57
+ end
58
+
59
+ def camelize_key(key)
60
+ key.to_s.camelize(:lower).to_sym
61
+ end
62
+
63
+ def metadata_by_string_or_regexp(metadata_map, string_or_regexp, metadata, opts = {})
64
+ arg_ref = "#{opts[:arg_name]} argument".strip
65
+ if metadata.blank?
66
+ fail "Valid #{arg_ref} required!" unless string_or_regexp.respond_to?(:to_sym)
67
+ response_metadata = {}
68
+ (metadata_map || {}).each do |key, item_metadata|
69
+ if key.is_a?(Regexp)
70
+ next unless string_or_regexp =~ key
71
+ else
72
+ next unless string_or_regexp.casecmp(key) == 0
73
+ end
74
+ OpenApi::Endpoints.merge_metadata(response_metadata, item_metadata.deep_dup)
75
+ end
76
+ return response_metadata
77
+ end
78
+ unless string_or_regexp.respond_to?(:to_sym) || string_or_regexp.is_a?(Regexp)
79
+ fail "Valid #{arg_ref} required!"
80
+ end
81
+ fail 'Expected Hash metadata_map argument!' unless metadata_map.is_a?(Hash)
82
+ fail 'Expected Hash metadata argument!' unless metadata.is_a?(Hash)
83
+ string_or_regexp = string_or_regexp.to_s unless string_or_regexp.is_a?(Regexp)
84
+ existing_metadata = (metadata_map[string_or_regexp] ||= {})
85
+ OpenApi::Endpoints.merge_metadata(existing_metadata, metadata)
86
+ nil
87
+ end
88
+
89
+ def open_api_type_and_format(type_name)
90
+ case type_name.to_s.downcase.to_sym
91
+ when :integer then [:integer, :int32]
92
+ when :long then [:integer, :int]
93
+ when :float then [:number, :float]
94
+ when :double then [:number, :double]
95
+ when :string then [:string, nil]
96
+ when :byte then [:string, :byte]
97
+ when :binary then [:string, :binary]
98
+ when :boolean then [:boolean, nil]
99
+ when :date then [:string, :date]
100
+ when :datetime then [:string, :'date-time']
101
+ when :password then [:string, :password]
102
+ else [nil, nil]
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def merge_hash_entry(hash, key, value, opts)
109
+ merge_by_hash = opts[:merge_by] || {}
110
+ if (merge_by = merge_by_hash[key]).present?
111
+ if merge_by.respond_to?(:call) && merge_by.respond_to?(:parameters)
112
+ if (param_count = merge_by.parameters.size) < 2
113
+ fail "Expected 2+ parameters (existing/merged values) for '#{key}' merge_by proc!"
114
+ end
115
+ merge_by.send(*([:call, hash[key], value, opts][0..param_count]))
116
+ end
117
+ elsif hash[key].is_a?(Hash) && value.is_a?(Hash)
118
+ if opts[:recursive_merge]
119
+ merge_hash(hash[key], value, opts)
120
+ else
121
+ hash[key].merge!(value)
122
+ end
123
+ elsif value.nil?
124
+ hash.delete(key)
125
+ else
126
+ hash[key] = value
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
data/lib/open-api.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'open-api/controller.rb'
2
+ require 'open-api/endpoints.rb'
3
+ require 'open-api/generator.rb'
4
+ require 'open-api/objects.rb'
5
+ require 'open-api/tags.rb'
6
+ require 'open-api/utils.rb'
7
+
8
+ module OpenApi
9
+ class << self
10
+ def configure(metadata = nil, &block)
11
+ return unless metadata.is_a?(Hash) || block_given?
12
+ global_metadata = @open_api_global_metadata || default_global_metadata
13
+ if metadata.is_a?(Hash)
14
+ global_metadata = OpenApi::Utils.merge_hash(global_metadata, metadata)
15
+ end
16
+ if block_given?
17
+ config = OpenStruct.new(global_metadata)
18
+ block.call(config)
19
+ global_metadata = OpenApi::Utils.merge_hash(global_metadata, config.to_h.symbolize_keys)
20
+ end
21
+ @open_api_global_metadata = global_metadata
22
+ end
23
+
24
+ def global_metadata
25
+ @open_api_global_metadata || default_global_metadata
26
+ end
27
+
28
+ def default_global_metadata
29
+ {
30
+ swagger: 2.0,
31
+ schemes: [:http]
32
+ }
33
+ end
34
+ end
35
+ end
data/open-api.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'open-api'
6
+ s.version = '0.8.2'
7
+ s.summary = 'Inline Openi API documentation for Ruby on Rails'
8
+ s.description = 'Provides the ability to specify Open API documentation inline within the ' \
9
+ 'source code of your Ruby on Rails project, utilizing a rake task to generate / maintain ' \
10
+ 'that documentation as required.'
11
+ s.licenses = ['Apache 2']
12
+ s.authors = ['Matthew Mead']
13
+ s.email = 'm.mead@precisionhawk.com'
14
+
15
+ s.files = Dir.glob("{lib,spec,config}/**/*")
16
+ s.files += %w(open-api.gemspec README.md)
17
+
18
+ s.require_path = "lib"
19
+
20
+ s.add_dependency "rails", ">= 4.0"
21
+
22
+ s.add_development_dependency "rspec-rails"
23
+ s.add_development_dependency "factory_girl"
24
+ end
metadata CHANGED
@@ -1,15 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Mead
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-27 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2016-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: factory_girl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
13
55
  description: Provides the ability to specify Open API documentation inline within
14
56
  the source code of your Ruby on Rails project, utilizing a rake task to generate
15
57
  / maintain that documentation as required.
@@ -17,7 +59,16 @@ email: m.mead@precisionhawk.com
17
59
  executables: []
18
60
  extensions: []
19
61
  extra_rdoc_files: []
20
- files: []
62
+ files:
63
+ - README.md
64
+ - lib/open-api.rb
65
+ - lib/open-api/controller.rb
66
+ - lib/open-api/endpoints.rb
67
+ - lib/open-api/generator.rb
68
+ - lib/open-api/objects.rb
69
+ - lib/open-api/tags.rb
70
+ - lib/open-api/utils.rb
71
+ - open-api.gemspec
21
72
  homepage:
22
73
  licenses:
23
74
  - Apache 2