betterdocs 0.7.1 → 0.8.0

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
  SHA256:
3
- metadata.gz: 84f61a235deb0348813dd4ccfc102e41e08ce374b3bcc7d48991935d08f1ce17
4
- data.tar.gz: dee9b3cfb105cea04192377d5f1b11306aee02da2165bc85c78eeaa2d239089c
3
+ metadata.gz: 7324105b58e4926bdbcec0e372efda88e523c1c31fb4f0181e9d4cebc94b135c
4
+ data.tar.gz: 6926a2ea9814cae5e763113badf138104f2fd9ae250c57e9cc8a16fb6eb18d43
5
5
  SHA512:
6
- metadata.gz: ca0afd3ada7b6852b0dda51e73457f37144b0c851fba52da52177ab81f76d22afe9f322b17f9e5a1d5fb965c7c0c4d0439a708fc112dd18de474880a2b97c711
7
- data.tar.gz: c095d79714edc2c0cc66eae7bd7c76b4f6791d989f20826a0081e6c7661d928744dfd260d4be03884a6955bcfbffcbe1639fc52b02993b952e0f06c75b9b5117
6
+ metadata.gz: ccaab04fbb9dfc351d79b9b722d1430c1a8afc5007f3f8bfd68d2d7d1c06ab4d26cf843e745750d1eb805b3d83122fb6b48597a8c1a3bc127fee5bc95e43443e
7
+ data.tar.gz: 67968de292f5c9e0d17b0d2669d72ca75f1e6d709ea219de4f36beab3b99e14452dfa626ff4cb0fe6222419ba99e19f3be4f4765242dbd8d9f0622839d150cf9
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .DS_Store
4
4
  .bundle
5
5
  .byebug_history
6
+ .history
6
7
  .ruby-version
7
8
  .rvmrc
8
9
  .utilsrc
@@ -0,0 +1,6 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Betterdocs",
4
+ "infobar"
5
+ ]
6
+ }
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ GemHadar do
12
12
  test_dir 'spec'
13
13
  ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', 'coverage', '.rvmrc',
14
14
  '.ruby-version', '.AppleDouble', 'tags', '.DS_Store', '.utilsrc',
15
- '.bundle', '.byebug_history', 'errors.lst', '.yardoc'
15
+ '.bundle', '.byebug_history', 'errors.lst', '.yardoc', '.history'
16
16
  readme 'README.md'
17
17
  title "#{name.camelize}"
18
18
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.1
1
+ 0.8.0
data/betterdocs.gemspec CHANGED
@@ -1,18 +1,18 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: betterdocs 0.7.1 ruby lib
2
+ # stub: betterdocs 0.8.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "betterdocs".freeze
6
- s.version = "0.7.1"
6
+ s.version = "0.8.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["betterplace Developers".freeze]
11
- s.date = "2021-06-01"
11
+ s.date = "2021-07-07"
12
12
  s.description = "This library provides tools to generate API documention for a web site's REST-ful JSON API.".freeze
13
13
  s.email = "developers@betterplace.org".freeze
14
- s.extra_rdoc_files = ["README.md".freeze, "lib/betterdocs.rb".freeze, "lib/betterdocs/controller_collector.rb".freeze, "lib/betterdocs/dsl.rb".freeze, "lib/betterdocs/dsl/common.rb".freeze, "lib/betterdocs/dsl/controller.rb".freeze, "lib/betterdocs/dsl/controller/action.rb".freeze, "lib/betterdocs/dsl/controller/action/param.rb".freeze, "lib/betterdocs/dsl/controller/action/response.rb".freeze, "lib/betterdocs/dsl/controller/controller.rb".freeze, "lib/betterdocs/dsl/controller/controller_base.rb".freeze, "lib/betterdocs/dsl/json_params.rb".freeze, "lib/betterdocs/dsl/json_params/param.rb".freeze, "lib/betterdocs/dsl/json_type_mapper.rb".freeze, "lib/betterdocs/dsl/naming.rb".freeze, "lib/betterdocs/dsl/representer.rb".freeze, "lib/betterdocs/dsl/result.rb".freeze, "lib/betterdocs/dsl/result/collection_property.rb".freeze, "lib/betterdocs/dsl/result/link.rb".freeze, "lib/betterdocs/dsl/result/property.rb".freeze, "lib/betterdocs/generator/config_shortcuts.rb".freeze, "lib/betterdocs/generator/markdown.rb".freeze, "lib/betterdocs/global.rb".freeze, "lib/betterdocs/json_params_representer.rb".freeze, "lib/betterdocs/json_params_representer_collector.rb".freeze, "lib/betterdocs/json_time_with_zone.rb".freeze, "lib/betterdocs/mix_into_controller.rb".freeze, "lib/betterdocs/railtie.rb".freeze, "lib/betterdocs/rake_tasks.rb".freeze, "lib/betterdocs/representer.rb".freeze, "lib/betterdocs/result_representer.rb".freeze, "lib/betterdocs/result_representer_collector.rb".freeze, "lib/betterdocs/sanitizer.rb".freeze, "lib/betterdocs/section.rb".freeze, "lib/betterdocs/version.rb".freeze]
15
- s.files = [".codeclimate.yml".freeze, ".gitignore".freeze, ".rspec".freeze, ".semaphore/semaphore.yml".freeze, ".tool-versions".freeze, "COPYING".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "betterdocs.gemspec".freeze, "lib/betterdocs.rb".freeze, "lib/betterdocs/controller_collector.rb".freeze, "lib/betterdocs/dsl.rb".freeze, "lib/betterdocs/dsl/common.rb".freeze, "lib/betterdocs/dsl/controller.rb".freeze, "lib/betterdocs/dsl/controller/action.rb".freeze, "lib/betterdocs/dsl/controller/action/param.rb".freeze, "lib/betterdocs/dsl/controller/action/response.rb".freeze, "lib/betterdocs/dsl/controller/controller.rb".freeze, "lib/betterdocs/dsl/controller/controller_base.rb".freeze, "lib/betterdocs/dsl/json_params.rb".freeze, "lib/betterdocs/dsl/json_params/param.rb".freeze, "lib/betterdocs/dsl/json_type_mapper.rb".freeze, "lib/betterdocs/dsl/naming.rb".freeze, "lib/betterdocs/dsl/representer.rb".freeze, "lib/betterdocs/dsl/result.rb".freeze, "lib/betterdocs/dsl/result/collection_property.rb".freeze, "lib/betterdocs/dsl/result/link.rb".freeze, "lib/betterdocs/dsl/result/property.rb".freeze, "lib/betterdocs/generator/config_shortcuts.rb".freeze, "lib/betterdocs/generator/markdown.rb".freeze, "lib/betterdocs/generator/markdown/templates/README.md.erb".freeze, "lib/betterdocs/generator/markdown/templates/section.md.erb".freeze, "lib/betterdocs/global.rb".freeze, "lib/betterdocs/json_params_representer.rb".freeze, "lib/betterdocs/json_params_representer_collector.rb".freeze, "lib/betterdocs/json_time_with_zone.rb".freeze, "lib/betterdocs/mix_into_controller.rb".freeze, "lib/betterdocs/railtie.rb".freeze, "lib/betterdocs/rake_tasks.rb".freeze, "lib/betterdocs/representer.rb".freeze, "lib/betterdocs/result_representer.rb".freeze, "lib/betterdocs/result_representer_collector.rb".freeze, "lib/betterdocs/sanitizer.rb".freeze, "lib/betterdocs/section.rb".freeze, "lib/betterdocs/tasks/doc.rake".freeze, "lib/betterdocs/version.rb".freeze, "spec/assets/app/assets/images/logos/logo.png".freeze, "spec/assets/app/controllers/api/foos_controller.rb".freeze, "spec/assets/app/views/api_v4/documentation/assets/CHANGELOG.md".freeze, "spec/assets/config/betterdocs.yml".freeze, "spec/betterdocs/dsl/controller/action/param_spec.rb".freeze, "spec/betterdocs/dsl/controller/action/response_spec.rb".freeze, "spec/betterdocs/dsl/json_type_mapper_spec.rb".freeze, "spec/betterdocs/dsl/result/collection_property_spec.rb".freeze, "spec/betterdocs/dsl/result/link_spec.rb".freeze, "spec/betterdocs/dsl/result/property_spec.rb".freeze, "spec/betterdocs/generator/markdown_spec.rb".freeze, "spec/betterdocs/global_spec.rb".freeze, "spec/betterdocs/json_params_representer_spec.rb".freeze, "spec/betterdocs/result_representer_spec.rb".freeze, "spec/betterdocs/sanitizer_spec.rb".freeze, "spec/controller_dsl_spec.rb".freeze, "spec/result_representer_dsl_spec.rb".freeze, "spec/spec_helper.rb".freeze]
14
+ s.extra_rdoc_files = ["README.md".freeze, "lib/betterdocs.rb".freeze, "lib/betterdocs/controller_collector.rb".freeze, "lib/betterdocs/dsl.rb".freeze, "lib/betterdocs/dsl/common.rb".freeze, "lib/betterdocs/dsl/controller.rb".freeze, "lib/betterdocs/dsl/controller/action.rb".freeze, "lib/betterdocs/dsl/controller/action/param.rb".freeze, "lib/betterdocs/dsl/controller/action/response.rb".freeze, "lib/betterdocs/dsl/controller/controller.rb".freeze, "lib/betterdocs/dsl/controller/controller_base.rb".freeze, "lib/betterdocs/dsl/json_params.rb".freeze, "lib/betterdocs/dsl/json_params/param.rb".freeze, "lib/betterdocs/dsl/json_type_mapper.rb".freeze, "lib/betterdocs/dsl/naming.rb".freeze, "lib/betterdocs/dsl/representer.rb".freeze, "lib/betterdocs/dsl/result.rb".freeze, "lib/betterdocs/dsl/result/collection_property.rb".freeze, "lib/betterdocs/dsl/result/link.rb".freeze, "lib/betterdocs/dsl/result/property.rb".freeze, "lib/betterdocs/generator/config_shortcuts.rb".freeze, "lib/betterdocs/generator/markdown.rb".freeze, "lib/betterdocs/generator/swagger.rb".freeze, "lib/betterdocs/global.rb".freeze, "lib/betterdocs/json_params_representer.rb".freeze, "lib/betterdocs/json_params_representer_collector.rb".freeze, "lib/betterdocs/json_time_with_zone.rb".freeze, "lib/betterdocs/mix_into_controller.rb".freeze, "lib/betterdocs/railtie.rb".freeze, "lib/betterdocs/rake_tasks.rb".freeze, "lib/betterdocs/representer.rb".freeze, "lib/betterdocs/result_representer.rb".freeze, "lib/betterdocs/result_representer_collector.rb".freeze, "lib/betterdocs/sanitizer.rb".freeze, "lib/betterdocs/section.rb".freeze, "lib/betterdocs/version.rb".freeze]
15
+ s.files = [".codeclimate.yml".freeze, ".gitignore".freeze, ".rspec".freeze, ".semaphore/semaphore.yml".freeze, ".tool-versions".freeze, ".vscode/settings.json".freeze, "COPYING".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "betterdocs.gemspec".freeze, "lib/betterdocs.rb".freeze, "lib/betterdocs/controller_collector.rb".freeze, "lib/betterdocs/dsl.rb".freeze, "lib/betterdocs/dsl/common.rb".freeze, "lib/betterdocs/dsl/controller.rb".freeze, "lib/betterdocs/dsl/controller/action.rb".freeze, "lib/betterdocs/dsl/controller/action/param.rb".freeze, "lib/betterdocs/dsl/controller/action/response.rb".freeze, "lib/betterdocs/dsl/controller/controller.rb".freeze, "lib/betterdocs/dsl/controller/controller_base.rb".freeze, "lib/betterdocs/dsl/json_params.rb".freeze, "lib/betterdocs/dsl/json_params/param.rb".freeze, "lib/betterdocs/dsl/json_type_mapper.rb".freeze, "lib/betterdocs/dsl/naming.rb".freeze, "lib/betterdocs/dsl/representer.rb".freeze, "lib/betterdocs/dsl/result.rb".freeze, "lib/betterdocs/dsl/result/collection_property.rb".freeze, "lib/betterdocs/dsl/result/link.rb".freeze, "lib/betterdocs/dsl/result/property.rb".freeze, "lib/betterdocs/generator/config_shortcuts.rb".freeze, "lib/betterdocs/generator/markdown.rb".freeze, "lib/betterdocs/generator/markdown/templates/README.md.erb".freeze, "lib/betterdocs/generator/markdown/templates/section.md.erb".freeze, "lib/betterdocs/generator/swagger.rb".freeze, "lib/betterdocs/generator/swagger_static/index.html".freeze, "lib/betterdocs/global.rb".freeze, "lib/betterdocs/json_params_representer.rb".freeze, "lib/betterdocs/json_params_representer_collector.rb".freeze, "lib/betterdocs/json_time_with_zone.rb".freeze, "lib/betterdocs/mix_into_controller.rb".freeze, "lib/betterdocs/railtie.rb".freeze, "lib/betterdocs/rake_tasks.rb".freeze, "lib/betterdocs/representer.rb".freeze, "lib/betterdocs/result_representer.rb".freeze, "lib/betterdocs/result_representer_collector.rb".freeze, "lib/betterdocs/sanitizer.rb".freeze, "lib/betterdocs/section.rb".freeze, "lib/betterdocs/tasks/doc.rake".freeze, "lib/betterdocs/version.rb".freeze, "spec/assets/app/assets/images/logos/logo.png".freeze, "spec/assets/app/controllers/api/foos_controller.rb".freeze, "spec/assets/app/views/api_v4/documentation/assets/CHANGELOG.md".freeze, "spec/assets/config/betterdocs.yml".freeze, "spec/betterdocs/dsl/controller/action/param_spec.rb".freeze, "spec/betterdocs/dsl/controller/action/response_spec.rb".freeze, "spec/betterdocs/dsl/json_type_mapper_spec.rb".freeze, "spec/betterdocs/dsl/result/collection_property_spec.rb".freeze, "spec/betterdocs/dsl/result/link_spec.rb".freeze, "spec/betterdocs/dsl/result/property_spec.rb".freeze, "spec/betterdocs/generator/markdown_spec.rb".freeze, "spec/betterdocs/global_spec.rb".freeze, "spec/betterdocs/json_params_representer_spec.rb".freeze, "spec/betterdocs/result_representer_spec.rb".freeze, "spec/betterdocs/sanitizer_spec.rb".freeze, "spec/controller_dsl_spec.rb".freeze, "spec/result_representer_dsl_spec.rb".freeze, "spec/spec_helper.rb".freeze]
16
16
  s.homepage = "http://github.com/betterplace/betterdocs".freeze
17
17
  s.rdoc_options = ["--title".freeze, "Betterdocs".freeze, "--main".freeze, "README.md".freeze]
18
18
  s.rubygems_version = "3.2.15".freeze
data/lib/betterdocs.rb CHANGED
@@ -26,6 +26,7 @@ require 'betterdocs/section'
26
26
  require 'betterdocs/mix_into_controller'
27
27
  require 'betterdocs/generator/config_shortcuts'
28
28
  require 'betterdocs/generator/markdown'
29
+ require 'betterdocs/generator/swagger'
29
30
  require 'betterdocs/rake_tasks'
30
31
  require 'betterdocs/json_time_with_zone'
31
32
  defined? Rails and require 'betterdocs/railtie'
@@ -1,3 +1,5 @@
1
+ require 'active_support/time_with_zone'
2
+
1
3
  require 'betterdocs/dsl/naming'
2
4
 
3
5
  class Betterdocs::Dsl::JsonParams::Param < Betterdocs::Dsl::Representer
@@ -1,3 +1,5 @@
1
+ require 'active_support/time_with_zone'
2
+
1
3
  require 'betterdocs/dsl/representer'
2
4
  require 'betterdocs/dsl/common'
3
5
  require 'betterdocs/dsl/naming'
@@ -0,0 +1,405 @@
1
+ require 'infobar'
2
+ require 'fileutils'
3
+ require 'term/ansicolor'
4
+ require 'json'
5
+
6
+ module Betterdocs
7
+ module Generator
8
+ class Swagger
9
+ include ::Betterdocs::Generator::ConfigShortcuts
10
+ include Term::ANSIColor
11
+ include FileUtils
12
+
13
+ def initialize(only: nil)
14
+ only and @only = Regexp.new(only)
15
+ end
16
+
17
+ def generate
18
+ if dir = config.swagger_output_directory.full?
19
+ generate_to dir
20
+ else
21
+ raise 'Specify an output_directory in your configuration!'
22
+ end
23
+ end
24
+
25
+ def generate_to(dirname)
26
+ map = { openapi: '3.0.2', paths: {}, components: { schemas: {} } }
27
+ add_error_envelope_schema(map[:components][:schemas])
28
+ configure_for_creation
29
+ prepare_dir(dirname)
30
+ add_global_info(map)
31
+ create_sections(map)
32
+ create_swagger(map, dirname)
33
+ create_assets
34
+ self
35
+ end
36
+
37
+ def configure_for_creation
38
+ infobar.puts color(40, "Setting asset_host to #{Betterdocs::Global.asset_host.inspect}.")
39
+ Betterdocs.rails.configuration.action_controller.asset_host = Betterdocs::Global.asset_host
40
+ options = {
41
+ host: Betterdocs::Global.api_host,
42
+ protocol: Betterdocs::Global.api_protocol,
43
+ }
44
+ infobar.puts color(40, "Setting default_url_options to #{options.inspect}.")
45
+ Betterdocs.rails.application.routes.default_url_options = options
46
+ self
47
+ end
48
+
49
+ def add_global_info(map)
50
+ slugs = get_request_url_slugs(sections.values.first.first.request)
51
+ map[:info] = { title: project_name, version: slugs[:ver] }
52
+ map[:servers] =
53
+ [{ url: "#{slugs[:protocol]}://#{[slugs[:host], slugs[:lang], "api_#{slugs[:ver]}"].join('/')}" }]
54
+ end
55
+
56
+ def create_sections(map)
57
+ Infobar(total: sections.size)
58
+ sections.values.each do |section|
59
+ infobar.progress(
60
+ message: " Section #{section.name.to_s.inspect} %c/%t in %te ETA %e @%E ",
61
+ force: true,
62
+ )
63
+ @only =~ section.name or next if @only
64
+
65
+ map = get_section_data(map, section)
66
+ end
67
+ infobar.finish message: ' %t sections created in %te, completed @%E '
68
+ infobar.newline
69
+ self
70
+ end
71
+
72
+ def create_assets
73
+ Infobar(total: config.swagger_assets.size)
74
+ config.each_swagger_asset do |src, dst|
75
+ infobar.progress(
76
+ message: " Asset #{File.basename(src).inspect} %c/%t in %te ETA %e @%E ",
77
+ force: true,
78
+ )
79
+ mkdir_p File.dirname(dst)
80
+ cp Betterdocs.rails.root.join(src), dst
81
+ end
82
+ infobar.finish message: ' %t assets created in %te, completed @%E '
83
+ infobar.newline
84
+ self
85
+ end
86
+
87
+ def create_swagger(map, dirname)
88
+ name = 'swagger.json'
89
+ cd dirname do
90
+ infobar.puts color(40, 'Creating swagger.')
91
+ json = JSON.pretty_generate(map)
92
+ render_to(name, json)
93
+ end
94
+ self
95
+ end
96
+
97
+ def fail_while_rendering(exception)
98
+ message = color(
99
+ 231,
100
+ on_color(124, " *** ERROR #{exception.class}: #{exception.message.inspect} ***")
101
+ )
102
+ infobar.puts message
103
+ exit 1
104
+ end
105
+
106
+ def add_param(hash, name, param_in, required = false, type = nil, description = '', schema = nil)
107
+ hash[:parameters] ||= []
108
+ p = { in: param_in, required: required, description: description, name: name }
109
+ p[:schema] = { type: type } unless type.nil?
110
+ p[:schema] = schema unless schema.nil?
111
+ hash[:parameters].push(p)
112
+ hash
113
+ end
114
+
115
+ def add_default_list_params(hash)
116
+ p = add_param(hash, 'page', 'query', false, 'integer')
117
+ add_param(p, 'per_page', 'query', false, 'integer')
118
+ end
119
+
120
+ def get_list_response_wrapper_schema
121
+ {
122
+ type: 'object',
123
+ properties: {
124
+ total_entries: { type: 'integer' },
125
+ offset: { type: 'integer' },
126
+ total_pages: { type: 'integer' },
127
+ current_page: { type: 'integer' },
128
+ per_page: { type: 'integer' },
129
+ },
130
+ required: %w[total_entries offset current_page per_page total_pages data],
131
+ }
132
+ end
133
+
134
+ def get_param_value_type(value)
135
+ case value
136
+ when String then 'string'
137
+ when Integer then 'integer'
138
+ when TrueClass, FalseClass then 'boolean'
139
+ when Float then 'number'
140
+ when Array then 'array'
141
+ else 'object'
142
+ end
143
+ end
144
+
145
+ def get_path_param_slug_type(slug)
146
+ return 'integer' if /\A[0-9]+\z/.match(slug)
147
+
148
+ 'string'
149
+ end
150
+
151
+ def add_error_envelope_schema(definitions)
152
+ definitions['ErrorEnvelope'] = {
153
+ type: 'object',
154
+ properties: {
155
+ name: { type: 'string' },
156
+ status: { type: 'string' },
157
+ status_code: { type: 'integer' },
158
+ reason: { type: 'string' },
159
+ backtrace: { type: 'array', items: { type: 'string' } },
160
+ message: { type: 'string' },
161
+ errors: { type: 'object' },
162
+ links: { type: 'array', items: { type: 'string' } },
163
+ },
164
+ required: %w[name status status_code reason backtrace message links],
165
+ }
166
+ end
167
+
168
+ def get_error_envelope_schema_ref
169
+ { schema: get_schema_ref('ErrorEnvelope') }
170
+ end
171
+
172
+ def get_request_url_slugs(request)
173
+ slugs = %r{
174
+ (?<method>GET|POST|PUT|DELETE|OPTIONS)\s+
175
+ (?<protocol>https?)://
176
+ (?<host>[a-z.]+)/
177
+ (?<lang>[a-z]{2})/api_
178
+ (?<ver>v[0-9]+)/
179
+ (?<path>[\w/]+\.json?)
180
+ }x.match(request).named_captures.transform_keys(&:to_sym)
181
+ slugs[:params] = []
182
+ split = slugs[:path].split('/')
183
+ path_cmp = split.map.with_index do |cmp, i|
184
+ unless i.even?
185
+ suffix = cmp['.json'] || ''
186
+ name = "#{split[i - 1]}_id"
187
+ cmp = "{#{name}}#{suffix}"
188
+ param = { name: name, type: get_path_param_slug_type(cmp.gsub('.json', '')) }
189
+ slugs[:params].push(param)
190
+ end
191
+ cmp
192
+ end
193
+ slugs[:method] = slugs[:method].downcase
194
+ slugs[:path] = "/#{path_cmp.join('/')}"
195
+ slugs
196
+ end
197
+
198
+ def get_request_definition(params)
199
+ schema = { type: 'object', properties: {}, required: [] }
200
+ params.each do |param|
201
+ schema[:properties][param.name] = get_schema(param.types, nil, param.description)
202
+ schema[:required].push(param.name) if param.required == true || param.required == 'yes'
203
+ end
204
+ schema
205
+ end
206
+
207
+ def get_name(title)
208
+ /([\w\s]+)/.match(title)[1]
209
+ end
210
+
211
+ def get_type(types)
212
+ type = types
213
+ nullable = false
214
+ if types.instance_of? Array
215
+ case types.length
216
+ when 0
217
+ type = nil
218
+ when 1
219
+ type = types[0]
220
+ else
221
+ types.each do |t|
222
+ if t == 'null'
223
+ nullable = true
224
+ else
225
+ type = t
226
+ end
227
+ end
228
+ end
229
+ end
230
+ unless %w[integer number string boolean array object].include?(type)
231
+ puts "Warning: invalid type #{type || 'nil'}"
232
+ end
233
+ [type, nullable]
234
+ end
235
+
236
+ def get_deprecated_from_description(description)
237
+ description.include?('DEPRECATED')
238
+ end
239
+
240
+ def get_schema(types, sub_cls, description)
241
+ type, nullable = get_type(types)
242
+ deprecated = get_deprecated_from_description(description)
243
+ res = { description: description, type: type, nullable: nullable, deprecated: deprecated }
244
+ case type
245
+ when 'array'
246
+ items = { type: 'string' }
247
+ items = get_schema_ref(sub_cls) if sub_cls
248
+ res[:items] = items
249
+ when 'object'
250
+ res = get_schema_ref(sub_cls) if sub_cls
251
+ res[:description] = description
252
+ res[:deprecated] = deprecated
253
+ end
254
+ res[:nullable] = true if nullable
255
+ res
256
+ end
257
+
258
+ def get_dto_name(representer)
259
+ representer&.name&.demodulize
260
+ end
261
+
262
+ def add_links_definition(definitions, cls, value)
263
+ definition = initialise_definition(definitions, cls)
264
+ definition[:properties][:links] ||= {
265
+ type: 'array',
266
+ nullable: false,
267
+ items: {
268
+ type: 'object',
269
+ properties: {
270
+ rel: { type: 'string', enum: [] },
271
+ href: { type: 'string' },
272
+ },
273
+ required: %w[rel href],
274
+ },
275
+ }
276
+ enum = definition[:properties][:links][:items][:properties][:rel][:enum]
277
+ enum.push(value) unless enum.include?(value)
278
+ end
279
+
280
+ def initialise_definition(definitions, cls)
281
+ definitions[cls] ||= { type: 'object', properties: {} }
282
+ end
283
+
284
+ def add_response_schema(definitions, response)
285
+ return nil unless response.full?
286
+
287
+ subs = {}
288
+ resp_cls = get_dto_name(response.representer)
289
+ (response.full?(:properties) || []).each do |p|
290
+ subs[p.full_name] = p.sub_representer? if p.sub_representer?
291
+ sub_cls = get_dto_name(subs[p.nesting_name])
292
+ cls = sub_cls || resp_cls
293
+ definition = initialise_definition(definitions, cls)
294
+ definition[:properties][p.public_name] = get_schema(p.types, get_dto_name(p.sub_representer?), p.description)
295
+ end
296
+ (response.full?(:links) || []).each do |l|
297
+ sub_cls = get_dto_name(subs[l.nesting_name])
298
+ add_links_definition(definitions, sub_cls || resp_cls, l.public_name)
299
+ end
300
+ get_schema_ref(resp_cls)
301
+ end
302
+
303
+ def get_schema_ref(name)
304
+ {
305
+ "$ref": "#/components/schemas/#{name}",
306
+ }
307
+ end
308
+
309
+ def add_required_to_definitions(definitions)
310
+ definitions.each do |def_key, d|
311
+ req = d[:properties].select { |_k, v| v.key?(:nullable) }.keys
312
+ definitions[def_key][:required] = req unless req.empty?
313
+ end
314
+ end
315
+
316
+ def add_body(definitions, params, action, name)
317
+ if action.json_params.full?
318
+ payload_name = "#{name.titleize.gsub(/\s/, '')}Request"
319
+ definitions[payload_name] = get_request_definition(action.json_params.values)
320
+ params[:requestBody] = wrap_content_object({ schema: get_schema_ref(payload_name) }, "#{name} Request")
321
+ end
322
+ end
323
+
324
+ def add_path_params(params, from_path)
325
+ from_path.each do |param|
326
+ add_param(params, param[:name], 'path', true, param[:type], param[:name])
327
+ end
328
+ end
329
+
330
+ def add_query_params(params, from_query, is_a_list)
331
+ from_query.each do |param|
332
+ add_param(params, param.name, 'query', param.required, get_param_value_type(param.value),
333
+ param.description)
334
+ end
335
+ add_default_list_params(params) if is_a_list
336
+ end
337
+
338
+ def wrap_content_object(object, description)
339
+ { content: { "application/json": object }, description: description }
340
+ end
341
+
342
+ def add_example(obj, name, example)
343
+ obj[:examples] ||= {}
344
+ obj[:examples][name] = { value: example }
345
+ end
346
+
347
+ def get_section_data(map, section)
348
+ section.each do |action|
349
+ name = get_name(action.title)
350
+ p = { description: action.description, summary: name }
351
+ is_a_list = action.response.data.instance_of?(ApiTools::ResultSet)
352
+ slugs = get_request_url_slugs(action.request)
353
+ add_path_params(p, slugs[:params])
354
+ add_query_params(p, action.params.values, is_a_list)
355
+ add_body(map[:components][:schemas], p, action, name)
356
+ item = add_response_schema(map[:components][:schemas], action.response)
357
+ schema = item
358
+ if is_a_list
359
+ list = get_list_response_wrapper_schema
360
+ list[:properties][:data] = { type: 'array', items: item }
361
+ schema = list
362
+ end
363
+ ok = { schema: schema }
364
+ unless action.response.nil?
365
+ add_example(ok, 'example', JSON.parse(JSON.pretty_generate(action.response, quirks_mode: true)))
366
+ end
367
+ p[:responses] =
368
+ { "200": wrap_content_object(ok, 'OK'),
369
+ default: wrap_content_object(get_error_envelope_schema_ref, 'Error') }
370
+ wrapped = {}
371
+ wrapped = map[slugs[:path]] if map.key?(slugs[:path])
372
+ wrapped[slugs[:method]] = p
373
+ map[:paths][slugs[:path]] = wrapped
374
+ end
375
+ add_required_to_definitions(map[:components][:schemas])
376
+ map
377
+ end
378
+
379
+ def render_to(filename, content)
380
+ File.open(filename, 'w') do |output|
381
+ output.write(content)
382
+ end
383
+ self
384
+ rescue StandardError => e
385
+ fail_while_rendering(e)
386
+ end
387
+
388
+ def prepare_dir(dirname)
389
+ dirname.present? or raise ArgumentError,
390
+ "#{dirname.inspect} should be an explicite output dirname"
391
+ begin
392
+ stat = File.stat(dirname)
393
+ if stat.directory?
394
+ rm_rf Dir[dirname.to_s + '/**/*']
395
+ else
396
+ raise ArgumentError, "#{dirname.inspect} is not a directory"
397
+ end
398
+ rescue Errno::ENOENT
399
+ end
400
+ mkdir_p dirname.to_s
401
+ self
402
+ end
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,66 @@
1
+ <!-- HTML for static distribution bundle build -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <title>Swagger UI</title>
7
+
8
+ <link
9
+ rel="stylesheet"
10
+ href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.8.1/swagger-ui.css"
11
+ type="text/css"
12
+ charset="UTF-8"
13
+ />
14
+ <style>
15
+ html {
16
+ box-sizing: border-box;
17
+ overflow: -moz-scrollbars-vertical;
18
+ overflow-y: scroll;
19
+ }
20
+
21
+ *,
22
+ *:before,
23
+ *:after {
24
+ box-sizing: inherit;
25
+ }
26
+
27
+ body {
28
+ margin: 0;
29
+ background: #fafafa;
30
+ }
31
+ </style>
32
+ </head>
33
+
34
+ <body>
35
+ <div id="swagger-ui"></div>
36
+
37
+ <script
38
+ src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.8.1/swagger-ui-bundle.js"
39
+ charset="UTF-8"
40
+ ></script>
41
+ <script
42
+ src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.8.1/swagger-ui-standalone-preset.js"
43
+ charset="UTF-8"
44
+ ></script>
45
+ <script>
46
+ window.onload = function () {
47
+ fetch("./swagger.json")
48
+ .then(resp => resp.json())
49
+ .then(spec => {
50
+ const ui = SwaggerUIBundle({
51
+ spec: spec,
52
+ dom_id: "#swagger-ui",
53
+ deepLinking: true,
54
+ presets: [
55
+ SwaggerUIBundle.presets.apis,
56
+ SwaggerUIStandalonePreset,
57
+ ],
58
+ plugins: [SwaggerUIBundle.plugins.DownloadUrl],
59
+ layout: "BaseLayout",
60
+ });
61
+ window.ui = ui;
62
+ });
63
+ };
64
+ </script>
65
+ </body>
66
+ </html>
@@ -27,7 +27,7 @@ module Betterdocs
27
27
  platform_host host
28
28
  __send__("#{prefix}_host", host)
29
29
  else
30
- [ __send__("#{prefix}_protocol"), __send__("#{prefix}_host") ] * '://'
30
+ [__send__("#{prefix}_protocol"), __send__("#{prefix}_host")].join('://')
31
31
  end
32
32
  end
33
33
 
@@ -41,7 +41,7 @@ module Betterdocs
41
41
 
42
42
  dsl_accessor :api_protocol do platform_protocol end # Protocol the API understands
43
43
 
44
- dsl_accessor :api_host do platform_host end # Hostname of the API (eventuallly with port number)
44
+ dsl_accessor :api_host do platform_host end # Hostname of the API (eventuallly with port number)
45
45
 
46
46
  def api_url(url = nil)
47
47
  handle_url(:api, url)
@@ -55,7 +55,7 @@ module Betterdocs
55
55
  handle_url(:asset, url)
56
56
  end
57
57
 
58
- dsl_accessor :api_default_format, 'json'
58
+ dsl_accessor :api_default_format, 'json'
59
59
 
60
60
  def api_base_url
61
61
  "#{api_protocol}://#{api_host}/#{api_prefix}"
@@ -65,9 +65,11 @@ module Betterdocs
65
65
  { protocol: api_protocol, host: api_host, format: api_default_format }
66
66
  end
67
67
 
68
- dsl_accessor :templates_directory # Template directory, where customised templates live if any exist
68
+ dsl_accessor :templates_directory # Template directory, where customised templates live if any exist
69
69
 
70
- dsl_accessor :output_directory, 'api_docs' # Output directory, where the api docs are created
70
+ dsl_accessor :output_directory, 'api_docs' # Output directory, where the api docs are created
71
+
72
+ dsl_accessor :swagger_output_directory, 'swagger_docs' # Output directory, where the swagger docs are created
71
73
 
72
74
  dsl_accessor :publish_git # URL to the git repo to which the docs are pushed
73
75
 
@@ -89,6 +91,17 @@ module Betterdocs
89
91
  @assets
90
92
  end
91
93
 
94
+ def swagger_assets=(hash)
95
+ @swagger_assets&.clear
96
+ swagger_assets(hash)
97
+ end
98
+
99
+ def swagger_assets(hash = nil)
100
+ @swagger_assets ||= {}
101
+ hash&.each { |path, to| swagger_asset path, to: to }
102
+ @swagger_assets
103
+ end
104
+
92
105
  # Defines an asset for the file at +path+. If +to+ was given it will be
93
106
  # copied to this path (it includes the basename) below
94
107
  # +templates_directory+ in the output, otherwise it will be copied
@@ -100,14 +113,25 @@ module Betterdocs
100
113
  elsif to == :root
101
114
  @assets[path.to_s] = to
102
115
  else
103
- raise ArgumentError, "keyword argument to needs to be a string or :root"
116
+ raise ArgumentError, 'keyword argument to needs to be a string or :root'
117
+ end
118
+ end
119
+
120
+ def swagger_asset(path, to: :root)
121
+ @swagger_assets ||= {}
122
+ if destination = to.ask_and_send(:to_str)
123
+ @swagger_assets[path.to_s] = destination
124
+ elsif to == :root
125
+ @swagger_assets[path.to_s] = to
126
+ else
127
+ raise ArgumentError, 'keyword argument to needs to be a string or :root'
104
128
  end
105
129
  end
106
130
 
107
131
  # Maps the assets original source path to its destination path in the
108
132
  # output by yielding to every asset's source/destination pair.
109
133
  def each_asset
110
- for (path, destination) in assets
134
+ assets.each do |(path, destination)|
111
135
  path = path.to_s
112
136
  if destination == :root
113
137
  yield path, File.join(output_directory.to_s, File.basename(path))
@@ -117,6 +141,17 @@ module Betterdocs
117
141
  end
118
142
  end
119
143
 
144
+ def each_swagger_asset
145
+ swagger_assets.each do |(path, destination)|
146
+ path = path.to_s
147
+ if destination == :root
148
+ yield path, File.join(swagger_output_directory.to_s, File.basename(path))
149
+ else
150
+ yield path, File.join(swagger_output_directory.to_s, destination.to_str)
151
+ end
152
+ end
153
+ end
154
+
120
155
  def configuration_file
121
156
  cc.betterdocs
122
157
  rescue ComplexConfig::ConfigurationFileMissing
@@ -192,7 +227,8 @@ module Betterdocs
192
227
 
193
228
  def url_for(options = {})
194
229
  Betterdocs.rails.application.routes.url_for(
195
- options | Betterdocs::Global.config.api_url_options)
230
+ options | Betterdocs::Global.config.api_url_options
231
+ )
196
232
  end
197
233
  end
198
234
  end
@@ -1,6 +1,6 @@
1
1
  namespace :doc do
2
- desc "Create the API documentation"
3
- task :api => :'doc:api:sandbox' do
2
+ desc 'Create the API documentation'
3
+ task api: :'doc:api:sandbox' do
4
4
  Betterdocs::Global.config do |config|
5
5
  Betterdocs::Generator::Markdown.new(only: ENV['ONLY']).generate
6
6
  cd config.output_directory do
@@ -15,39 +15,46 @@ namespace :doc do
15
15
 
16
16
  namespace :api do
17
17
  desc 'Let database transactions run in a sandboxed environment'
18
- task :sandbox => [:'doc:set_betterdocs_env', :environment] do
19
-
18
+ task sandbox: %i[doc:set_betterdocs_env environment] do
20
19
  ActiveRecord::Base.connection.begin_db_transaction
21
20
  at_exit do
22
- ActiveRecord::Base.connection.rollback_db_transaction
21
+ ActiveRecord::Base.connection.rollback_db_transaction
23
22
  end
24
23
  end
25
24
 
26
- desc "Push the newly created API documentation to the remote git repo"
27
- task :push => :api do
25
+ desc 'Create the API swagger documentation'
26
+ task swagger: :'doc:api:sandbox' do
28
27
  Betterdocs::Global.config do |config|
29
- config.publish_git or fail "Configure a git repo as publish_git to publish to"
28
+ Betterdocs::Generator::Swagger.new(only: ENV['ONLY']).generate
29
+ cd config.swagger_output_directory do
30
+ File.open('.gitignore', 'w') { |ignore| ignore.puts config.ignore }
31
+ end
32
+ end
33
+ end
34
+
35
+ desc 'Push the newly created API documentation to the remote git repo'
36
+ task push: :api do
37
+ Betterdocs::Global.config do |config|
38
+ config.publish_git or raise 'Configure a git repo as publish_git to publish to'
30
39
  cd config.output_directory do
31
- File.directory?('.git') or sh "git init"
32
- sh "git remote rm publish_git || true"
40
+ File.directory?('.git') or sh 'git init'
41
+ sh 'git remote rm publish_git || true'
33
42
  sh "git remote add publish_git #{config.publish_git}"
34
- sh "git add -A"
43
+ sh 'git add -A'
35
44
  sh 'git commit -m "Add some more changes to API documentation" || true'
36
45
  sh 'git push -f publish_git master'
37
46
  end
38
47
  end
39
48
  end
40
49
 
41
- desc "Publish the newly created API documentation"
42
- task :publish => [ :push ]
50
+ desc 'Publish the newly created API documentation'
51
+ task publish: [:push]
43
52
 
44
- desc "Publish and view the newly created API documentation"
45
- task :view => :publish do
53
+ desc 'Publish and view the newly created API documentation'
54
+ task view: :publish do
46
55
  Betterdocs::Global.config do |config|
47
56
  url = config.publish_git
48
- if url !~ /\Ahttps?:/
49
- url.sub!(/.*?([^@]*):/, 'http://\1/')
50
- end
57
+ url.sub!(/.*?([^@]*):/, 'http://\1/') if url !~ /\Ahttps?:/
51
58
  sh "open #{url.inspect}"
52
59
  end
53
60
  end
@@ -1,6 +1,6 @@
1
1
  module Betterdocs
2
2
  # Betterdocs version
3
- VERSION = '0.7.1'
3
+ VERSION = '0.8.0'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: betterdocs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - betterplace Developers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-01 00:00:00.000000000 Z
11
+ date: 2021-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gem_hadar
@@ -190,6 +190,7 @@ extra_rdoc_files:
190
190
  - lib/betterdocs/dsl/result/property.rb
191
191
  - lib/betterdocs/generator/config_shortcuts.rb
192
192
  - lib/betterdocs/generator/markdown.rb
193
+ - lib/betterdocs/generator/swagger.rb
193
194
  - lib/betterdocs/global.rb
194
195
  - lib/betterdocs/json_params_representer.rb
195
196
  - lib/betterdocs/json_params_representer_collector.rb
@@ -209,6 +210,7 @@ files:
209
210
  - ".rspec"
210
211
  - ".semaphore/semaphore.yml"
211
212
  - ".tool-versions"
213
+ - ".vscode/settings.json"
212
214
  - COPYING
213
215
  - Gemfile
214
216
  - LICENSE
@@ -239,6 +241,8 @@ files:
239
241
  - lib/betterdocs/generator/markdown.rb
240
242
  - lib/betterdocs/generator/markdown/templates/README.md.erb
241
243
  - lib/betterdocs/generator/markdown/templates/section.md.erb
244
+ - lib/betterdocs/generator/swagger.rb
245
+ - lib/betterdocs/generator/swagger_static/index.html
242
246
  - lib/betterdocs/global.rb
243
247
  - lib/betterdocs/json_params_representer.rb
244
248
  - lib/betterdocs/json_params_representer_collector.rb