json-schema-docs 0.1.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 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: []