json-schema-docs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cd5892af89b1910f3b373a462c2fb4854cf1626b15031f44a7f3d5b6b7280c3d
4
+ data.tar.gz: 65c3110d0ab1e4397fafca814423daae4bc5bf3c5e34bdb087c9fa346e0615f5
5
+ SHA512:
6
+ metadata.gz: b9dbb7897d5d73835f56ad12cdbacf9e0aa14ba97a6e7ee04c630ba0158bac274cc03b8216102b2034ba580d49b54352b3bfe7f65a3254c30a6814e5b6eeb94d
7
+ data.tar.gz: 98ba7a96d3e77c85d4ca35bf7f56254b35ad62cd6162978a3ce2d95bf20c35e3d9a2b17894a7457c1a8d625aa702ff255068eeaedfef5b71b6d601284c97b074
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /output/
11
+ test/json-schema-docs/fixtures/output
12
+ /.sass-cache/
13
+ /output/
14
+ .byebug_history
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ inherit_gem:
2
+ rubocop-standard:
3
+ - config/default.yml
4
+
5
+ Style/StringLiterals:
6
+ Enabled: true
7
+ EnforcedStyle: single_quotes
8
+
9
+ Naming/FileName:
10
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - 2.3.6
7
+ - 2.4.3
8
+ - 2.5.0
9
+ - 2.6.0
10
+
11
+ git:
12
+ depth: 10
13
+
14
+ before_install:
15
+ - gem update --system
16
+ - gem install bundler
17
+
18
+ matrix:
19
+ include:
20
+ - script: bundle exec rake rubocop
21
+ rvm: 2.6.0
22
+ - script: bundle exec rake html_proofer
23
+ rvm: 2.6.0
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in json-schema-docs.gemspec
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2019 Garen Torikian
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # json-schema-docs
2
+
3
+ Inspired by [prmd](https://github.com/interagent/prmd)'s doc generation, this is a stand-alone gem to generate Markdown files from a single JSON schema file. `prmd`'s doc generator is rather opinionated, and I did not like its opinions. 😅
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'json-schema-docs'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install json-schema-docs
20
+
21
+ ## Usage
22
+
23
+ ``` ruby
24
+ # pass in a filename
25
+ JsonSchemaDocs.build(filename: filename)
26
+
27
+ # or pass in a string
28
+ JsonSchemaDocs.build(schema: contents)
29
+
30
+ # or a schema class
31
+ schema = Prmd::Schema.new(data)
32
+ JsonSchemaDocs.build(schema: schema)
33
+ ```
34
+
35
+
36
+ ## Breakdown
37
+
38
+ There are several phases going on the single `JsonSchemaDocs.build` call:
39
+
40
+ * The JSON schema file is read (if you passed `filename`) through `Prmd::Schema` (or simply consumed if you passed it through `schema`).
41
+ * `JsonSchemaDocs::Parser` manipulates the schema into a slightly saner format.
42
+ * `JsonSchemaDocs::Generator` takes that saner format and converts it into Markdown, via ERB templates.
43
+
44
+ If you wanted to, you could break these calls up individually. For example:
45
+
46
+ ``` ruby
47
+ options = {}
48
+ options[:filename] = "#{File.dirname(__FILE__)}/../data/schema.json"
49
+
50
+ options = JsonSchemaDocs::Configuration::JSON_SCHEMA_DOCS.merge(options)
51
+
52
+ response = File.read(options[:filename])
53
+
54
+ parser = JsonSchemaDocs::Parser.new(response, options)
55
+ parsed_schema = parser.parse
56
+
57
+ generator = JsonSchemaDocs::Generator.new(parsed_schema, options)
58
+
59
+ generator.generate
60
+ ```
61
+
62
+ ## Generating output
63
+
64
+ By default, the generation process uses ERB to layout the content into Markdown.
65
+
66
+ ### Helper methods
67
+
68
+ In your ERB layouts, there are several helper methods you can use. The helper methods are:
69
+
70
+ * `slugify(str)` - This slugifies the given string.
71
+ * `include(filename, opts)` - This embeds a template from your `includes` folder, passing along the local options provided.
72
+
73
+ ## Configuration
74
+
75
+ The following options are available:
76
+
77
+ | Option | Description | Default |
78
+ | :----- | :---------- | :------ |
79
+ | `filename` | The location of your schema's IDL file. | `nil` |
80
+ | `schema` | A string representing a schema IDL file. | `nil` |
81
+ | `delete_output` | Deletes `output_dir` before generating content. | `false` |
82
+ | `output_dir` | The location of the output Markdown files. | `./output/` |
83
+ | `templates` | The templates to use when generating Markdown files. You may override any of the following keys: `endpoint`, `object`, `includes`. | The defaults are found in _lib/json-schema-docs/layouts/_.
84
+ | `prmd` | The options to pass into PRMD's parser. | The defaults are found in _lib/json-schema-docs/configuration.rb/_.
85
+
86
+ ## Development
87
+
88
+ After checking out the repo, run `script/bootstrap` to install dependencies. Then, run `rake test` to run the tests. You can also run `bundle exec rake console` for an interactive prompt that will allow you to experiment.
89
+
90
+ ## Sample site
91
+
92
+ Clone this repository and run:
93
+
94
+ ```
95
+ bundle exec rake sample
96
+ ```
97
+
98
+ to see some sample output.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+
5
+ require 'rubocop/rake_task'
6
+
7
+ RuboCop::RakeTask.new(:rubocop)
8
+
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'test'
11
+ t.libs << 'lib'
12
+ t.warning = false
13
+ t.test_files = FileList['test/**/*_test.rb']
14
+ end
15
+
16
+ task default: :test
17
+
18
+ desc 'Generate the documentation'
19
+ task :generate_sample do
20
+ require 'pry'
21
+ require 'json-schema-docs'
22
+
23
+ options = {}
24
+ options[:delete_output] = true
25
+ options[:filename] = File.join(File.dirname(__FILE__), 'test', 'json-schema-docs', 'fixtures', 'heroku.json')
26
+
27
+ JsonSchemaDocs.build(options)
28
+ end
29
+
30
+ desc 'Generate the documentation and run a web server'
31
+ task sample: [:generate_sample] do
32
+ require 'webrick'
33
+
34
+ puts 'Navigate to http://localhost:3000 to see the sample docs'
35
+
36
+ mime_types = WEBrick::HTTPUtils::DefaultMimeTypes
37
+ mime_types.store 'md', 'text/plain'
38
+
39
+ options = {
40
+ Port: 3000,
41
+ MimeTypes: mime_types
42
+ }
43
+ server = WEBrick::HTTPServer.new options
44
+ server.mount '/', WEBrick::HTTPServlet::FileHandler, 'output'
45
+ trap('INT') { server.stop }
46
+ server.start
47
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'json-schema-docs/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'json-schema-docs'
9
+ spec.version = JsonSchemaDocs::VERSION
10
+ spec.authors = ['Garen Torikian']
11
+ spec.email = ['gjtorikian@gmail.com']
12
+
13
+ spec.summary = 'Easily generate Markdown files from your JSON schema.'
14
+ spec.homepage = 'https://github.com/gjtorikian/json-schema-docs'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'bin'
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'prmd', '~> 0.13'
25
+
26
+ spec.add_development_dependency 'awesome_print'
27
+ spec.add_development_dependency 'html-proofer', '~> 3.4'
28
+ spec.add_development_dependency 'minitest', '~> 5.0'
29
+ spec.add_development_dependency 'minitest-focus', '~> 1.1'
30
+ spec.add_development_dependency 'pry-byebug', '~> 3.6'
31
+ spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'rubocop'
33
+ spec.add_development_dependency 'rubocop-performance'
34
+ spec.add_development_dependency 'rubocop-standard'
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ module JsonSchemaDocs
3
+ module Configuration
4
+ JSON_SCHEMA_DOCS_DEFAULTS = {
5
+ # initialize
6
+ filename: nil,
7
+ schema: nil,
8
+
9
+ # Generating
10
+ delete_output: false,
11
+ output_dir: './output/',
12
+
13
+ templates: {
14
+ endpoint: "#{File.dirname(__FILE__)}/layouts/endpoint.md.erb",
15
+ object: "#{File.dirname(__FILE__)}/layouts/object.md.erb",
16
+
17
+ includes: "#{File.dirname(__FILE__)}/layouts/includes",
18
+ },
19
+
20
+ prmd: {
21
+ http_header: {},
22
+ content_type: 'application/json',
23
+ doc: {
24
+ url_style: 'default'
25
+ }
26
+ }
27
+ }.freeze
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ require 'erb'
3
+
4
+ module JsonSchemaDocs
5
+ class Generator
6
+ include Helpers
7
+
8
+ attr_accessor :parsed_schema
9
+
10
+ def initialize(parsed_schema, options)
11
+ @parsed_schema = parsed_schema
12
+ @options = options
13
+
14
+ %i(endpoint object).each do |sym|
15
+ if !File.exist?(@options[:templates][sym])
16
+ raise IOError, "`#{sym}` template #{@options[:templates][sym]} was not found"
17
+ end
18
+ instance_variable_set("@json_schema_#{sym}_template", ERB.new(File.read(@options[:templates][sym]), nil, '-'))
19
+ end
20
+ end
21
+
22
+ def generate
23
+ FileUtils.rm_rf(@options[:output_dir]) if @options[:delete_output]
24
+
25
+ @parsed_schema.each_pair do |resource, schemata|
26
+ %i(endpoint object).each do |type|
27
+ contents = render(type, schemata)
28
+ write_file(type, resource, contents)
29
+
30
+ contents = render(type, schemata)
31
+ write_file(type, resource, contents)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def render(type, schemata)
39
+ layout = instance_variable_get("@json_schema_#{type}_template")
40
+ opts = @options.merge(helper_methods)
41
+
42
+ opts[:schemata] = schemata
43
+ layout.result(OpenStruct.new(opts).instance_eval { binding })
44
+ end
45
+
46
+ def write_file(type, name, contents, trim: true)
47
+ if type == :object
48
+ path = File.join(@options[:output_dir], 'objects', name.downcase)
49
+ FileUtils.mkdir_p(path)
50
+ elsif type == :endpoint
51
+ path = File.join(@options[:output_dir], 'objects', name.downcase, 'endpoints')
52
+ FileUtils.mkdir_p(path)
53
+ end
54
+
55
+ if trim
56
+ # normalize spacing so that CommonMarker doesn't treat it as `pre`
57
+ contents = contents.gsub(/^\s+$/, '')
58
+ contents = contents.gsub(/^\s{4}/m, ' ')
59
+ end
60
+
61
+ filename = File.join(path, 'index.md')
62
+ File.write(filename, contents) unless contents.nil?
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonSchemaDocs
4
+ module Helpers
5
+ SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze
6
+
7
+ attr_accessor :templates
8
+
9
+ def slugify(str)
10
+ slug = str.gsub(SLUGIFY_PRETTY_REGEXP, '-')
11
+ slug.gsub!(%r!^\-|\-$!i, '')
12
+ slug.downcase
13
+ end
14
+
15
+ def include(filename, opts = {})
16
+ template = fetch_include(filename)
17
+ opts = @options.merge(opts)
18
+ template.result(OpenStruct.new(opts.merge(helper_methods)).instance_eval { binding })
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_include(filename)
24
+ @templates ||= {}
25
+
26
+ return @templates[filename] unless @templates[filename].nil?
27
+
28
+ contents = File.read(File.join(@options[:templates][:includes], filename))
29
+
30
+ @templates[filename] = ERB.new(contents, nil, '-')
31
+ end
32
+
33
+ def helper_methods
34
+ return @helper_methods if defined?(@helper_methods)
35
+
36
+ @helper_methods = {}
37
+
38
+ Helpers.instance_methods.each do |name|
39
+ next if name == :helper_methods
40
+ @helper_methods[name] = method(name)
41
+ end
42
+
43
+ @helper_methods
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ # <a class="header-link" name="resource-<%= schemata['title'] %>"><%= schemata['title'] %></a>
2
+
3
+ <% (schemata['links'] || []).each do |link, datum| %>
4
+
5
+ ## <a class="header-link" name="link-<%= link['method'] %>-<%= resource %>-<%= link['href'] %>"><%= schemata['title'] %> <%= link['title'] %></a>
6
+
7
+ <%= link['description'] %>
8
+
9
+ ```
10
+ <%= link['method'] %> <%= link['link_path'] %>
11
+ ```
12
+
13
+ <% if link['required_properties'] && !link['required_properties'].empty? %>
14
+ ## Required Parameters
15
+
16
+ | Name | Type | Description | Example |
17
+ | ------- | ------- | ------- | ------- |
18
+ <%- link['required_properties'].each do |property_ref| -%>
19
+ | **<%= property_ref[:name] %>** | *<%= property_ref[:type] %>* | <%= property_ref[:description] %> | <%= property_ref[:example] %> |
20
+ <%- end -%>
21
+
22
+ <% end %>
23
+
24
+ <% if link['optional_properties'] && !link['optional_properties'].empty? %>
25
+ ## Optional Parameters
26
+
27
+ | Name | Type | Description | Example |
28
+ | ------- | ------- | ------- | ------- |
29
+ <%- link['optional_properties'].each do |property_ref| -%>
30
+ | **<%= property_ref[:name] %>** | *<%= property_ref[:type] %>* | <%= property_ref[:description] %> | <%= property_ref[:example] %> |
31
+ <%- end -%>
32
+
33
+ <% end %>
34
+
35
+ <%= include.('example.md.erb', example: link['example']) %>
36
+
37
+ <%= include.('response.md.erb', response: link['response']) %>
38
+
39
+ <% end %>
@@ -0,0 +1,11 @@
1
+ ## Curl Example
2
+
3
+ ```bash
4
+ $ curl -n <%= example[:request] -%><%- unless example[:http_headers].empty? %> \<%- end -%>
5
+ <%- if !example[:data].nil? %>
6
+ <%= example[:data] -%>
7
+ <%- end -%>
8
+ <%- example[:http_headers].each do |key, value| %>
9
+ -H "<%= key %>: <%= value %>"<%- if key != example[:http_headers].keys.last %> \<%- end -%>
10
+ <%- end %>
11
+ ```
@@ -0,0 +1,11 @@
1
+ ## Response Example
2
+
3
+ ```
4
+ <%= response[:header] %>
5
+ ```
6
+
7
+ <% unless response[:example].nil? %>
8
+ ```json
9
+ <%= response[:example] %>
10
+ ```
11
+ <% end %>
@@ -0,0 +1,17 @@
1
+ # <a class="header-link" name="resource-<%= schemata['title'].downcase %>"><%= schemata['title'] %></a>
2
+
3
+ <% if schemata['stability'] && !schemata['stability'].empty? %>
4
+ Stability: `<%= schemata['stability'] %>`
5
+ <% end %>
6
+
7
+ <%= schemata['description'] %>
8
+
9
+ <% if schemata['property_refs'] && !schemata['property_refs'].empty? %>
10
+ ## Attributes
11
+
12
+ | Name | Type | Description | Example |
13
+ | ------- | ------- | ------- | ------- |
14
+ <%- schemata['property_refs'].each do |property_ref| -%>
15
+ | **<%= property_ref[:name] %>** | *<%= property_ref[:type] %>* | <%= property_ref[:description] %> | <%= property_ref[:example] %> |
16
+ <%- end -%>
17
+ <% end %>
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+ require 'prmd'
3
+
4
+ module JsonSchemaDocs
5
+ class Parser
6
+ include Helpers
7
+
8
+ attr_reader :processed_schema
9
+
10
+ def initialize(schema, options)
11
+ @options = options
12
+
13
+ if schema.is_a?(Prmd::Schema)
14
+ @schema = schema
15
+ else
16
+ # FIXME: Multiloader has issues: https://github.com/interagent/prmd/issues/279
17
+ # for now just always assume a JSON file
18
+ data = Prmd::MultiLoader::Json.load_data(schema)
19
+
20
+ @schema = Prmd::Schema.new(data)
21
+ end
22
+
23
+ @processed_schema = {}
24
+ end
25
+
26
+ def parse
27
+ @schema['properties'].each_key do |key|
28
+ resource, property = key, @schema['properties'][key]
29
+ begin
30
+ _, schemata = @schema.dereference(property)
31
+
32
+ # establish condensed object description
33
+ if schemata['properties'] && !schemata['properties'].empty?
34
+ schemata['property_refs'] = []
35
+ refs = extract_schemata_refs(@schema, schemata['properties']).map { |v| v && v.split('/') }
36
+ extract_attributes(@schema, schemata['properties']).each_with_index do |(key, type, description, example), i|
37
+ property_ref = { type: type, description: description, example: example}
38
+ if refs[i] && refs[i][1] == 'definitions' && refs[i][2] != resource
39
+ property_ref[:name] = '[%s](#%s)' % [key, 'resource-' + refs[i][2]]
40
+ else
41
+ property_ref[:name] = key
42
+ end
43
+ schemata['property_refs'].push(property_ref)
44
+ end
45
+ end
46
+
47
+ # establish full link description
48
+ (schemata['links'] || []).each do |link, datum|
49
+ link_path = build_link_path(@schema, link)
50
+ response_example = link['response_example']
51
+
52
+ if link.has_key?('schema') && link['schema'].has_key?('properties')
53
+ required, optional = Prmd::Link.new(link).required_and_optional_parameters
54
+
55
+ unless required.empty?
56
+ link_schema_required_properties = extract_attributes(@schema, required).map do |(name, type, description, example)|
57
+ { name: name, type: type, description: description, example: example}
58
+ end
59
+ end
60
+
61
+ unless optional.empty?
62
+ link_schema_optional_properties = extract_attributes(@schema, optional).map do |(name, type, description, example)|
63
+ { name: name, type: type, description: description, example: example}
64
+ end
65
+ end
66
+ end
67
+
68
+ link['link_path'] = link_path
69
+ link['required_properties'] = link_schema_required_properties
70
+ link['optional_properties'] = link_schema_optional_properties
71
+ link['example'] = generate_example(link, link_path)
72
+ link['response'] = {
73
+ header: generate_response_header(response_example, link),
74
+ example: generate_response_example(response_example, link, resource)
75
+ }
76
+ end
77
+
78
+ @processed_schema[resource] = schemata
79
+ rescue => e
80
+ $stdout.puts("Error in resource: #{resource}")
81
+ raise e
82
+ end
83
+ end
84
+
85
+ @processed_schema
86
+ end
87
+
88
+ private
89
+
90
+ def extract_attributes(schema, properties)
91
+ attributes = []
92
+
93
+ _, properties = schema.dereference(properties)
94
+
95
+ properties.each do |key, value|
96
+ # found a reference to another element:
97
+ _, value = schema.dereference(value)
98
+
99
+ # include top level reference to nested things, when top level is nullable
100
+ if value.has_key?('type') && value['type'].include?('null') && (value.has_key?('items') || value.has_key?('properties'))
101
+ attributes << build_attribute(schema, key, value)
102
+ end
103
+
104
+ if value.has_key?('anyOf')
105
+ descriptions = []
106
+ examples = []
107
+
108
+ anyof = value['anyOf']
109
+
110
+ anyof.each do |ref|
111
+ _, nested_field = schema.dereference(ref)
112
+ descriptions << nested_field['description'] if nested_field['description']
113
+ examples << nested_field['example'] if nested_field['example']
114
+ end
115
+
116
+ # avoid repetition :}
117
+ description = if descriptions.size > 1
118
+ descriptions.first.gsub!(/ of (this )?.*/, '')
119
+ descriptions[1..-1].map { |d| d.gsub!(/unique /, '') }
120
+ [descriptions[0...-1].join(', '), descriptions.last].join(' or ')
121
+ else
122
+ description = descriptions.last
123
+ end
124
+
125
+ example = [*examples].map { |e| "`#{e.to_json}`" }.join(' or ')
126
+ attributes << [key, 'string', description, example]
127
+
128
+ # found a nested object
129
+ elsif value['properties']
130
+ nested = extract_attributes(schema, value['properties'])
131
+ nested.each do |attribute|
132
+ attribute[0] = "#{key}:#{attribute[0]}"
133
+ end
134
+ attributes.concat(nested)
135
+
136
+ elsif array_with_nested_objects?(value['items'])
137
+ if value['items']['properties']
138
+ nested = extract_attributes(schema, value['items']['properties'])
139
+ nested.each do |attribute|
140
+ attribute[0] = "#{key}/#{attribute[0]}"
141
+ end
142
+ attributes.concat(nested)
143
+ end
144
+ if value['items']['oneOf']
145
+ value['items']['oneOf'].each_with_index do |oneof, index|
146
+ ref, oneof_definition = schema.dereference(oneof)
147
+ oneof_name = ref ? ref.split('/').last : index
148
+ nested = extract_attributes(schema, oneof_definition['properties'])
149
+ nested.each do |attribute|
150
+ attribute[0] = "#{key}/[#{oneof_name.upcase}].#{attribute[0]}"
151
+ end
152
+ attributes.concat(nested)
153
+ end
154
+ end
155
+
156
+ # just a regular attribute
157
+ else
158
+ attributes << build_attribute(schema, key, value)
159
+ end
160
+ end
161
+ attributes.map! { |key, type, description, example|
162
+ if example.nil? && Prmd::DefaultExamples.key?(type)
163
+ example = '`%s`' % Prmd::DefaultExamples[type].to_json
164
+ end
165
+ [key, type, description, example]
166
+ }
167
+ return attributes.sort
168
+ end
169
+
170
+ def extract_schemata_refs(schema, properties)
171
+ ret = []
172
+ properties.keys.sort.each do |key|
173
+ value = properties[key]
174
+ ref, value = schema.dereference(value)
175
+ if value['properties']
176
+ refs = extract_schemata_refs(schema, value['properties'])
177
+ elsif value['items'] && value['items']['properties']
178
+ refs = extract_schemata_refs(schema, value['items']['properties'])
179
+ else
180
+ refs = [ref]
181
+ end
182
+ if value.has_key?('type') && value['type'].include?('null') && (value.has_key?('items') || value.has_key?('properties'))
183
+ # A nullable object usually isn't a reference to another schema. It's
184
+ # either not a reference at all, or it's a reference within the same
185
+ # schema. Instead, the definition of the nullable object might contain
186
+ # references to specific properties.
187
+ #
188
+ # If all properties refer to the same schema, we'll use that as the
189
+ # reference. This might even overwrite an actual, intra-schema
190
+ # reference.
191
+
192
+ l = refs.map { |v| v && v.split('/')[0..2] }
193
+ if l.uniq.size == 1 && l[0] != nil
194
+ ref = l[0].join('/')
195
+ end
196
+ ret << ref
197
+ end
198
+ ret.concat(refs)
199
+ end
200
+ ret
201
+ end
202
+
203
+ def build_attribute(schema, key, value)
204
+ description = value['description'] || ''
205
+ if value['default']
206
+ description += "<br/> **default:** `#{value['default'].to_json}`"
207
+ end
208
+
209
+ if value['minimum'] || value['maximum']
210
+ description += '<br/> **Range:** `'
211
+ if value['minimum']
212
+ comparator = value['exclusiveMinimum'] ? '<' : '<='
213
+ description += "#{value['minimum'].to_json} #{comparator} "
214
+ end
215
+ description += 'value'
216
+ if value['maximum']
217
+ comparator = value['exclusiveMaximum'] ? '<' : '<='
218
+ description += " #{comparator} #{value['maximum'].to_json}"
219
+ end
220
+ description += '`'
221
+ end
222
+
223
+ if value['enum']
224
+ description += '<br/> **one of:**' + [*value['enum']].map { |e| "`#{e.to_json}`" }.join(' or ')
225
+ end
226
+
227
+ if value['pattern']
228
+ description += "<br/> **pattern:** `#{value['pattern']}`"
229
+ end
230
+
231
+ if value['minLength'] || value['maxLength']
232
+ description += '<br/> **Length:** `'
233
+ if value['minLength']
234
+ description += "#{value['minLength'].to_json}"
235
+ end
236
+ unless value['minLength'] == value['maxLength']
237
+ if value['maxLength']
238
+ unless value['minLength']
239
+ description += '0'
240
+ end
241
+ description += "..#{value['maxLength'].to_json}"
242
+ else
243
+ description += '..∞'
244
+ end
245
+ end
246
+ description += '`'
247
+ end
248
+
249
+ if value.has_key?('example')
250
+ example = if value['example'].is_a?(Hash) && value['example'].has_key?('oneOf')
251
+ value['example']['oneOf'].map { |e| "`#{e.to_json}`" }.join(' or ')
252
+ else
253
+ "`#{value['example'].to_json}`"
254
+ end
255
+ elsif (value['type'] == ['array'] && value.has_key?('items')) || value.has_key?('enum')
256
+ example = "`#{schema.schema_value_example(value).to_json}`"
257
+ elsif value['type'].include?('null')
258
+ example = '`null`'
259
+ end
260
+
261
+ type = if value['type'].include?('null')
262
+ 'nullable '
263
+ else
264
+ ''
265
+ end
266
+ type += (value['format'] || (value['type'] - ['null']).first)
267
+ [key, type, description, example]
268
+ end
269
+
270
+
271
+ def build_link_path(schema, link)
272
+ link['href'].gsub(%r|(\{\([^\)]+\)\})|) do |ref|
273
+ ref = ref.gsub('%2F', '/').gsub('%23', '#').gsub(%r|[\{\(\)\}]|, '')
274
+ ref_resource = ref.split('#/definitions/').last.split('/').first.gsub('-', '_')
275
+ identity_key, identity_value = schema.dereference(ref)
276
+ if identity_value.has_key?('anyOf')
277
+ '{' + ref_resource + '_' + identity_value['anyOf'].map { |r| r['$ref'].split('/').last }.join('_or_') + '}'
278
+ else
279
+ '{' + ref_resource + '_' + identity_key.split('/').last + '}'
280
+ end
281
+ end
282
+ end
283
+
284
+ def array_with_nested_objects?(items)
285
+ return unless items
286
+ items['properties'] || items['oneOf']
287
+ end
288
+
289
+ def generate_example(link, link_path)
290
+ request = {}
291
+ data = {}
292
+ headers = {}
293
+
294
+ path = link_path.gsub(/{([^}]*)}/) { |match| '$' + match.gsub(/[{}]/, '').upcase }
295
+ get_params = []
296
+
297
+ if link.has_key?('schema')
298
+ data = @schema.schema_example(link['schema'])
299
+ if link['method'].upcase == 'GET' && !data.nil?
300
+ get_params << Prmd::UrlGenerator.new({schema: @schema, link: link, options: @options[:prmd]}).url_params
301
+ end
302
+ end
303
+
304
+ data = nil if data.empty? # same thing
305
+
306
+ # fetch any headers
307
+ if link['method'].upcase != 'GET'
308
+ opts = @options[:prmd].dup
309
+ headers = { 'Content-Type' => opts[:content_type] }.merge(opts[:http_header])
310
+ end
311
+
312
+ # define initial request call
313
+ if link['method'].upcase != 'GET'
314
+ request = "-X #{link['method']} #{@schema.href}#{path}"
315
+ else
316
+ request = "#{@schema.href}#{path}"
317
+ end
318
+
319
+ # add data, if present
320
+ if !data.nil? && link['method'].upcase != 'GET'
321
+ data = "-d '#{JSON.pretty_generate(data)}' \\"
322
+ elsif !get_params.empty? && link['method'].upcase == 'GET'
323
+ data = "-G #{get_params.join(" ss\\\n -d ")} \\"
324
+ end
325
+
326
+ { request: request, data: data, http_headers: headers }
327
+ end
328
+
329
+ def generate_response_header(response_example, link)
330
+ return response_example['head'] if response_example
331
+
332
+ header = 'HTTP/1.1'
333
+ code = case link['rel']
334
+ when 'create'
335
+ '201 Created'
336
+ when 'empty'
337
+ '202 Accepted'
338
+ else
339
+ '200 OK'
340
+ end
341
+ "#{header} #{code}"
342
+ end
343
+
344
+ def generate_response_example(response_example, link, resource)
345
+ if response_example || link['rel'] != 'empty'
346
+ if response_example
347
+ response_example['body']
348
+ else
349
+ if link['rel'] == 'empty'
350
+ elsif link.has_key?('targetSchema')
351
+ JSON.pretty_generate(@schema.schema_example(link['targetSchema']))
352
+ elsif link['rel'] == 'instances'
353
+ JSON.pretty_generate([@schema.schemata_example(resource)])
354
+ else
355
+ JSON.pretty_generate(@schema.schemata_example(resource))
356
+ end
357
+ end
358
+ else
359
+ nil
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module JsonSchemaDocs
3
+ VERSION = '0.1.0'.freeze
4
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ require 'json-schema-docs/helpers'
3
+ require 'json-schema-docs/configuration'
4
+ require 'json-schema-docs/generator'
5
+ require 'json-schema-docs/parser'
6
+ require 'json-schema-docs/version'
7
+
8
+ begin
9
+ require 'awesome_print'
10
+ require 'pry'
11
+ rescue LoadError; end
12
+
13
+ module JsonSchemaDocs
14
+ class << self
15
+ def build(options)
16
+ options = JsonSchemaDocs::Configuration::JSON_SCHEMA_DOCS_DEFAULTS.merge(options)
17
+
18
+ filename = options[:filename]
19
+ schema = options[:schema]
20
+
21
+ if !filename.nil? && !schema.nil?
22
+ raise ArgumentError, 'Pass in `filename` or `schema`, but not both!'
23
+ end
24
+
25
+ if filename.nil? && schema.nil?
26
+ raise ArgumentError, 'Pass in either `filename` or `schema`'
27
+ end
28
+
29
+ if filename
30
+ unless filename.is_a?(String)
31
+ raise TypeError, "Expected `String`, got `#{filename.class}`"
32
+ end
33
+
34
+ unless File.exist?(filename)
35
+ raise ArgumentError, "#{filename} does not exist!"
36
+ end
37
+
38
+ schema = File.read(filename)
39
+ else
40
+ if !schema.is_a?(String) && !schema.is_a?(Prmd::Schema)
41
+ raise TypeError, "Expected `String` or `Prmd::Schema`, got `#{schema.class}`"
42
+ end
43
+
44
+ schema = schema
45
+ end
46
+
47
+ parser = JsonSchemaDocs::Parser.new(schema, options)
48
+ parsed_schema = parser.parse
49
+
50
+ generator = JsonSchemaDocs::Generator.new(parsed_schema, options)
51
+
52
+ generator.generate
53
+ end
54
+ end
55
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ bundle install "$@"
data/script/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ dir=`pwd`
6
+
7
+ echo "===> Bundling..."
8
+ script/bootstrap --quiet
9
+
10
+ echo "===> Launching..."
11
+ bundle exec rake console
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json-schema-docs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Garen Torikian
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prmd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.13'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: awesome_print
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: html-proofer
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-focus
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-performance
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-standard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description:
154
+ email:
155
+ - gjtorikian@gmail.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".gitignore"
161
+ - ".rubocop.yml"
162
+ - ".travis.yml"
163
+ - Gemfile
164
+ - LICENSE.txt
165
+ - README.md
166
+ - Rakefile
167
+ - json-schema-docs.gemspec
168
+ - lib/json-schema-docs.rb
169
+ - lib/json-schema-docs/configuration.rb
170
+ - lib/json-schema-docs/generator.rb
171
+ - lib/json-schema-docs/helpers.rb
172
+ - lib/json-schema-docs/layouts/endpoint.md.erb
173
+ - lib/json-schema-docs/layouts/includes/example.md.erb
174
+ - lib/json-schema-docs/layouts/includes/response.md.erb
175
+ - lib/json-schema-docs/layouts/object.md.erb
176
+ - lib/json-schema-docs/parser.rb
177
+ - lib/json-schema-docs/version.rb
178
+ - script/bootstrap
179
+ - script/console
180
+ homepage: https://github.com/gjtorikian/json-schema-docs
181
+ licenses:
182
+ - MIT
183
+ metadata: {}
184
+ post_install_message:
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ required_rubygems_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubygems_version: 3.0.6
200
+ signing_key:
201
+ specification_version: 4
202
+ summary: Easily generate Markdown files from your JSON schema.
203
+ test_files: []