Merge pull request #156 from eputnam/pdoc184

(PDOC-184) generate markdown
This commit is contained in:
Hunter Haugen 2018-02-20 15:28:34 -08:00
commit 7dff75642b
No known key found for this signature in database
GPG Key ID: EF99694AA599DDAD
31 changed files with 1659 additions and 32 deletions

View File

@ -162,17 +162,33 @@ Strings can produce documentation in JSON and then either generate a `.json` fil
To generate JSON documentation to a file, run:
```
$ puppet strings generate --emit-json documentation.json
$ puppet strings generate --format json --out documentation.json
```
To generate and then print JSON documentation to stdout, run:
```
$ puppet strings generate --emit-json-stdout
$ puppet strings generate --format json
```
For details about Strings JSON output, see [Strings JSON schema](https://github.com/puppetlabs/puppet-strings/blob/master/JSON.md).
### Output documents in Markdown
Strings can also produce documentation in Markdown and then either generate an `.md` file or print Markdown to stdout. The generated markdown layout has been reviewed and approved by Puppet's tech pubs team and is the same that is used in Puppet Supported modules.
To generate REFERENCE.md:
```
$ puppet strings generate --format markdown
```
To write Markdown documentation to another file, use the --out option:
```
$ puppet strings generate --format markdown --out docs/INFO.md
```
### Output documents to GitHub Pages
To generate documents and then make them available on [GitHub Pages](https://pages.github.com/), use the Strings rake task `strings:gh_pages:update`. See [Rake tasks](#rake-tasks) for setup and usage details.
@ -285,6 +301,35 @@ end
All provider method calls, including `confine`, `defaultfor`, and `commands`, are automatically parsed and documented by Strings. The `desc` method is used to generate the docstring, and can include tags such as `@example` if written as a heredoc.
Document types that use the new [Resource API](https://github.com/puppetlabs/puppet-resource_api):
```ruby
Puppet::ResourceApi.register_type(
name: 'database',
docs: 'An example database server resource type.',
attributes: {
ensure: {
type: 'Enum[present, absent, up, down]',
desc: 'What state the database should be in.',
default: 'up',
},
address: {
type: 'String',
desc: 'The database server name.',
behaviour: :namevar,
},
encrypt: {
type: 'Boolean',
desc: 'Whether or not to encrypt the database.',
default: false,
behaviour: :parameter,
},
},
)
```
Here, the `docs` key acts like the `desc` method of the traditional resource type. Everything else is the same, except that now everything is a value in the data structure, not passed to methods.
**Note**: Puppet Strings can not evaluate your Ruby code, so only certain static expressions are supported.
### Documenting functions

View File

@ -14,7 +14,9 @@ module PuppetStrings
# @option options [Boolean] :debug Enable YARD debug output.
# @option options [Boolean] :backtrace Enable YARD backtraces.
# @option options [String] :markup The YARD markup format to use (defaults to 'markdown').
# @option options [String] :json Enables JSON output to the given file. If the file is nil, STDOUT is used.
# @option options [String] :path Write the selected format to a file path
# @option options [Boolean] :markdown From the --format option, is the format Markdown?
# @option options [Boolean] :json Is the format JSON?
# @option options [Array<String>] :yard_args The arguments to pass to yard.
# @return [void]
def self.generate(search_patterns = DEFAULT_SEARCH_PATTERNS, options = {})
@ -27,15 +29,18 @@ module PuppetStrings
args << '--backtrace' if options[:backtrace]
args << "-m#{options[:markup] || 'markdown'}"
render_as_json = options.key? :json
json_file = nil
if render_as_json
json_file = options[:json]
file = nil
if options[:json] || options[:markdown]
file = if options[:json]
options[:path]
elsif options[:markdown]
options[:path] || "REFERENCE.md"
end
# Disable output and prevent stats/progress when writing to STDOUT
args << '-n'
args << '-q' unless json_file
args << '--no-stats' unless json_file
args << '--no-progress' unless json_file
args << '-q' unless file
args << '--no-stats' unless file
args << '--no-progress' unless file
end
yard_args = options[:yard_args]
@ -46,10 +51,24 @@ module PuppetStrings
YARD::CLI::Yardoc.run(*args)
# If outputting JSON, render the output
if render_as_json
require 'puppet-strings/json'
PuppetStrings::Json.render(json_file)
if options[:json]
render_json(file)
end
# If outputting Markdown, render the output
if options[:markdown]
render_markdown(file)
end
end
def self.render_json(path)
require 'puppet-strings/json'
PuppetStrings::Json.render(path)
end
def self.render_markdown(path)
require 'puppet-strings/markdown'
PuppetStrings::Markdown.render(path)
end
# Runs the YARD documentation server.

View File

@ -34,6 +34,13 @@ module PuppetStrings::Json
next t.to_hash if t.respond_to?(:to_hash)
tag = { tag_name: t.tag_name }
# grab nested information for @option tags
if tag[:tag_name] == 'option'
tag[:opt_name] = t.pair.name
tag[:opt_text] = t.pair.text
tag[:opt_types] = t.pair.types
tag[:parent] = t.name
end
tag[:text] = t.text if t.text
tag[:types] = t.types if t.types
tag[:name] = t.name if t.name

View File

@ -0,0 +1,35 @@
require 'puppet-strings/json'
# module for parsing Yard Registries and generating markdown
module PuppetStrings::Markdown
require_relative 'markdown/puppet_classes'
require_relative 'markdown/functions'
require_relative 'markdown/defined_types'
require_relative 'markdown/resource_types'
require_relative 'markdown/table_of_contents'
# generates markdown documentation
# @return [String] markdown doc
def self.generate
final = "# Reference\n\n"
final << PuppetStrings::Markdown::TableOfContents.render
final << PuppetStrings::Markdown::PuppetClasses.render
final << PuppetStrings::Markdown::DefinedTypes.render
final << PuppetStrings::Markdown::ResourceTypes.render
final << PuppetStrings::Markdown::Functions.render
final
end
# mimicks the behavior of the json render, although path will never be nil
# @param [String] path path to destination file
def self.render(path = nil)
if path.nil?
puts generate
exit
else
File.open(path, 'w') { |file| file.write(generate) }
YARD::Logger.instance.debug "Wrote markdown to #{path}"
end
end
end

View File

@ -0,0 +1,158 @@
require 'puppet-strings'
require 'puppet-strings/json'
require 'puppet-strings/yard'
module PuppetStrings::Markdown
# This class makes elements in a YARD::Registry hash easily accessible for templates.
#
# Here's an example hash:
#{:name=>:klass,
# :file=>"(stdin)",
# :line=>16,
# :inherits=>"foo::bar",
# :docstring=>
# {:text=>"An overview for a simple class.",
# :tags=>
# [{:tag_name=>"summary", :text=>"A simple class."},
# {:tag_name=>"since", :text=>"1.0.0"},
# {:tag_name=>"see", :name=>"www.puppet.com"},
# {:tag_name=>"example",
# :text=>
# "class { 'klass':\n" +
# " param1 => 1,\n" +
# " param3 => 'foo',\n" +
# "}",
# :name=>"This is an example"},
# {:tag_name=>"author", :text=>"eputnam"},
# {:tag_name=>"option", :name=>"opts"},
# {:tag_name=>"raise", :text=>"SomeError"},
# {:tag_name=>"param",
# :text=>"First param.",
# :types=>["Integer"],
# :name=>"param1"},
# {:tag_name=>"param",
# :text=>"Second param.",
# :types=>["Any"],
# :name=>"param2"},
# {:tag_name=>"param",
# :text=>"Third param.",
# :types=>["String"],
# :name=>"param3"}]},
# :defaults=>{"param1"=>"1", "param2"=>"undef", "param3"=>"'hi'"},
# :source=>
# "class klass (\n" +
# " Integer $param1 = 1,\n" +
# " $param2 = undef,\n" +
# " String $param3 = 'hi'\n" +
# ") inherits foo::bar {\n" +
# "}"}
class Base
def initialize(registry, component_type)
@type = component_type
@registry = registry
@tags = registry[:docstring][:tags] || []
end
# generate 1:1 tag methods
# e.g. {:tag_name=>"author", :text=>"eputnam"}
{ :return_val => 'return',
:since => 'since',
:summary => 'summary' }.each do |method_name, tag_name|
# @return [String] unless the tag is nil or the string.length == 0
define_method method_name do
@tags.select { |tag| tag[:tag_name] == "#{tag_name}" }[0][:text] unless @tags.select { |tag| tag[:tag_name] == "#{tag_name}" }[0].nil? || @tags.select { |tag| tag[:tag_name] == "#{tag_name}" }[0][:text].length.zero?
end
end
# @return [String] top-level name
def name
@registry[:name].to_s unless @registry[:name].nil?
end
# @return [String] 'Overview' text (untagged text)
def text
@registry[:docstring][:text] unless @registry[:docstring][:text].empty?
end
# @return [String] data type of return value
def return_type
@tags.select { |tag| tag[:tag_name] == 'return' }[0][:types][0] unless @tags.select { |tag| tag[:tag_name] == 'return' }[0].nil?
end
# @return [String] text from @since tag
def since
@tags.select { |tag| tag[:tag_name] == 'since' }[0][:text] unless @tags.select { |tag| tag[:tag_name] == 'since' }[0].nil?
end
# @return [Array] @see tag hashes
def see
@tags.select { |tag| tag[:tag_name] == 'see' } unless @tags.select { |tag| tag[:tag_name] == 'see' }[0].nil?
end
# @return [Array] parameter tag hashes
def params
@tags.select { |tag| tag[:tag_name] == 'param' } unless @tags.select { |tag| tag[:tag_name] == 'param' }[0].nil?
end
# @return [Array] example tag hashes
def examples
@tags.select { |tag| tag[:tag_name] == 'example' } unless @tags.select { |tag| tag[:tag_name] == 'example' }[0].nil?
end
# @return [Array] raise tag hashes
def raises
@tags.select { |tag| tag[:tag_name] == 'raise' } unless @tags.select { |tag| tag[:tag_name] == 'raise' }[0].nil?
end
# @return [Array] option tag hashes
def options
@tags.select { |tag| tag[:tag_name] == 'option' } unless @tags.select { |tag| tag[:tag_name] == 'option' }[0].nil?
end
# @param parameter_name
# parameter name to match to option tags
# @return [Array] option tag hashes that have a parent parameter_name
def options_for_param(parameter_name)
opts_for_p = options.select { |o| o[:parent] == parameter_name } unless options.nil?
opts_for_p unless opts_for_p.nil? || opts_for_p.length.zero?
end
# @return [Array] any defaults found for the component
def defaults
@registry[:defaults] unless @registry[:defaults].nil?
end
# @return [Hash] information needed for the table of contents
def toc_info
{
name: name.to_s,
link: link,
desc: summary || @registry[:docstring][:text].gsub("\n", ". ")
}
end
# @return [String] makes the component name suitable for a GitHub markdown link
def link
name.delete('::').strip.gsub(' ','-').downcase
end
# Some return, default, or valid values need to be in backticks. Instead of fu in the handler or code_object, this just does the change on the front.
# @param value
# any string
# @return [String] value or value in backticks if it is in a list
def value_string(value)
to_symbol = %w[undef true false]
if to_symbol.include? value
return "`#{value}`"
else
return value
end
end
# @return [String] full markdown rendering of a component
def render(template)
file = File.join(File.dirname(__FILE__),"templates/#{template}")
ERB.new(File.read(file), nil, '-').result(binding)
end
end
end

View File

@ -0,0 +1,14 @@
require 'puppet-strings/markdown/base'
module PuppetStrings::Markdown
class DefinedType < Base
def initialize(registry)
@template = 'classes_and_defines.erb'
super(registry, 'defined type')
end
def render
super(@template)
end
end
end

View File

@ -0,0 +1,29 @@
require_relative 'defined_type'
module PuppetStrings::Markdown
module DefinedTypes
# @return [Array] list of defined types
def self.in_dtypes
YARD::Registry.all(:puppet_defined_type).sort_by!(&:name).map!(&:to_hash)
end
def self.render
final = in_dtypes.length > 0 ? "## Defined types\n\n" : ""
in_dtypes.each do |type|
final << PuppetStrings::Markdown::DefinedType.new(type).render
end
final
end
def self.toc_info
final = []
in_dtypes.each do |type|
final.push(PuppetStrings::Markdown::DefinedType.new(type).toc_info)
end
final
end
end
end

View File

@ -0,0 +1,52 @@
require 'puppet-strings/markdown/base'
module PuppetStrings::Markdown
class Function < Base
attr_reader :signatures
def initialize(registry)
@template = 'function.erb'
super(registry, 'function')
@signatures = []
registry[:signatures].each do |sig|
@signatures.push(Signature.new(sig))
end
end
def render
super(@template)
end
def type
t = @registry[:type]
if t =~ /ruby4x/
"Ruby 4.x API"
elsif t =~ /ruby3/
"Ruby 3.x API"
elsif t =~ /ruby/
"Ruby"
else
"Puppet Language"
end
end
def error_type(r)
"`#{r.split(' ')[0]}`"
end
def error_text(r)
"#{r.split(' ').drop(1).join(' ')}"
end
end
class Function::Signature < Base
def initialize(registry)
@registry = registry
super(@registry, 'function signature')
end
def signature
@registry[:signature]
end
end
end

View File

@ -0,0 +1,30 @@
require_relative 'function'
module PuppetStrings::Markdown
module Functions
# @return [Array] list of functions
def self.in_functions
YARD::Registry.all(:puppet_function).sort_by!(&:name).map!(&:to_hash)
end
def self.render
final = in_functions.length > 0 ? "## Functions\n\n" : ""
in_functions.each do |func|
final << PuppetStrings::Markdown::Function.new(func).render
end
final
end
def self.toc_info
final = []
in_functions.each do |func|
final.push(PuppetStrings::Markdown::Function.new(func).toc_info)
end
final
end
end
end

View File

@ -0,0 +1,14 @@
require 'puppet-strings/markdown/base'
module PuppetStrings::Markdown
class PuppetClass < Base
def initialize(registry)
@template = 'classes_and_defines.erb'
super(registry, 'class')
end
def render
super(@template)
end
end
end

View File

@ -0,0 +1,29 @@
require_relative 'puppet_class'
module PuppetStrings::Markdown
module PuppetClasses
# @return [Array] list of classes
def self.in_classes
YARD::Registry.all(:puppet_class).sort_by!(&:name).map!(&:to_hash)
end
def self.render
final = in_classes.length > 0 ? "## Classes\n\n" : ""
in_classes.each do |klass|
final << PuppetStrings::Markdown::PuppetClass.new(klass).render
end
final
end
def self.toc_info
final = []
in_classes.each do |klass|
final.push(PuppetStrings::Markdown::PuppetClass.new(klass).toc_info)
end
final
end
end
end

View File

@ -0,0 +1,27 @@
require 'puppet-strings/markdown/base'
module PuppetStrings::Markdown
class ResourceType < Base
def initialize(registry)
@template = 'resource_type.erb'
super(registry, 'type')
end
def render
super(@template)
end
def properties
@registry[:properties]
end
def parameters
@registry[:parameters]
end
def regex_in_data_type?(data_type)
m = data_type.match(/\w+\[\/.*\/\]/)
m unless m.nil? || m.length.zero?
end
end
end

View File

@ -0,0 +1,29 @@
require_relative 'resource_type'
module PuppetStrings::Markdown
module ResourceTypes
# @return [Array] list of resource types
def self.in_rtypes
YARD::Registry.all(:puppet_type).sort_by!(&:name).map!(&:to_hash)
end
def self.render
final = in_rtypes.length > 0 ? "## Resource types\n\n" : ""
in_rtypes.each do |type|
final << PuppetStrings::Markdown::ResourceType.new(type).render
end
final
end
def self.toc_info
final = []
in_rtypes.each do |type|
final.push(PuppetStrings::Markdown::ResourceType.new(type).toc_info)
end
final
end
end
end

View File

@ -0,0 +1,13 @@
module PuppetStrings::Markdown
module TableOfContents
def self.render
puppet_classes = PuppetStrings::Markdown::PuppetClasses.toc_info
puppet_defined_types = PuppetStrings::Markdown::DefinedTypes.toc_info
puppet_resource_types = PuppetStrings::Markdown::ResourceTypes.toc_info
puppet_functions = PuppetStrings::Markdown::Functions.toc_info
template = File.join(File.dirname(__FILE__),"templates/table_of_contents.erb")
ERB.new(File.read(template), nil, '-').result(binding)
end
end
end

View File

@ -0,0 +1,53 @@
### <%= name %>
<% if since -%>
* **Since** <%= since %>
<% end -%>
<% if see -%>
* **See also**
<% see.each do |sa| -%>
<%= sa[:name] %>
<%= sa[:text] %>
<% end -%>
<% end -%>
<% if examples -%>
#### Examples
<% examples.each do |eg| -%>
##### <%= eg[:name] %>
```puppet
<%= eg[:text] %>
```
<% end -%>
<% end -%>
<% if params %>
#### Parameters
The following parameters are available in the `<%= name %>` <%= @type %>.
<% params.each do |param| -%>
##### `<%= param[:name] %>`
<% if param[:types] -%>
Data type: `<%= param[:types].join(', ') -%>`
<% end -%>
<%= param[:text] %>
<% if options_for_param(param[:name]) -%>
Options:
<% options_for_param(param[:name]).each do |o| -%>
* **<%= o[:opt_name] %>** `<%= o[:opt_types][0] %>`: <%= o[:opt_text] %>
<% end -%>
<% end -%>
<% if defaults && defaults[param[:name]] -%>
Default value: <%= value_string(defaults[param[:name]]) %>
<% end -%>
<% end -%>
<% end -%>

View File

@ -0,0 +1,40 @@
### <%= name %>
Type: <%= type %>
<% signatures.each do |sig| -%>
#### `<%= sig.signature %>`
<% if sig.text -%>
<%= sig.text %>
<% end -%>
<% if sig.return_type -%>
Returns: `<%= sig.return_type %>`<% if sig.return_val %> <%= sig.return_val %><% end %>
<% end -%>
<% if raises -%>
Raises:
<% raises.each do |r| -%>
* <%= error_type(r[:text]) %> <%= error_text(r[:text]) %>
<% end -%>
<% end -%>
<% if sig.params -%>
<% sig.params.each do |param| -%>
##### `<%= param[:name] %>`
Data type: `<%= param[:types][0] %>`
<%= param[:text] %>
<% if sig.options_for_param(param[:name]) -%>
Options:
<% sig.options_for_param(param[:name]).each do |o| -%>
* **<%= o[:opt_name] %>** `<%= o[:opt_types][0] %>`: <%= o[:opt_text] %>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<% end -%>

View File

@ -0,0 +1,107 @@
### <%= name %>
<% if since -%>
* **Since** <%= since %>
<% end -%>
<% if see -%>
* **See also**
<% see.each do |sa| -%>
<%= sa[:name] %>
<%= sa[:text] %>
<% end -%>
<% end -%>
<% if text -%>
<%= text %>
<% end %>
<% if examples -%>
#### Examples
<% examples.each do |eg| -%>
##### <%= eg[:name] %>
```puppet
<%= eg[:text] %>
```
<% end -%>
<% end -%>
<% if properties %>
#### Properties
The following properties are available in the `<%= name %>` <%= @type %>.
<% properties.each do |prop| -%>
##### `<%= prop[:name] %>`
<% if prop[:values] -%>
Valid values: <%= prop[:values].map { |value| value_string(value) }.join(', ') %>
<% end -%>
<% if prop[:isnamevar] -%>
namevar
<% end -%>
<% if prop[:aliases] -%>
Aliases: <%= prop[:aliases].to_s.delete('{').delete('}') %>
<% end -%>
<% if prop[:data_type] -%>
Data type: `<%= prop[:data_type] %>`<%= "\n_\*this data type contains a regex that may not be accurately reflected in generated documentation_" if regex_in_data_type?(prop[:data_type]) %>
<% end -%>
<%= prop[:description] %>
<% if options_for_param(prop[:name]) -%>
Options:
<% options_for_param(prop[:name]).each do |o| -%>
* **<%= o[:opt_name] %>** `<%= o[:opt_types][0] %>`: <%= o[:opt_text] %>
<% end -%>
<% end -%>
<% if prop[:default] -%>
Default value: <%= prop[:default] %>
<% end -%>
<% end -%>
<% end -%>
<% if parameters -%>
#### Parameters
The following parameters are available in the `<%= name %>` <%= @type %>.
<% parameters.each do |param| -%>
##### `<%= param[:name] %>`
<% if param[:values] -%>
Valid values: <%= param[:values].map { |value| value_string(value) }.join(', ') %>
<% end -%>
<% if param[:isnamevar] -%>
namevar
<% end -%>
<% if param[:aliases] -%>
Aliases: <%= param[:aliases].to_s.delete('{').delete('}') %>
<% end -%>
<% if param[:data_type] -%>
Data type: `<%= param[:data_type] %>`<%= "\n_\*this data type contains a regex that may not be accurately reflected in generated documentation_" if regex_in_data_type?(param[:data_type]) %>
<% end -%>
<%= param[:description] %>
<% if options_for_param(param[:name]) -%>
Options:
<% options_for_param(param[:name]).each do |o| -%>
* **<%= o[:opt_name] %>** `<%= o[:opt_types][0] %>`: <%= o[:opt_text] %>
<% end -%>
<% end -%>
<% if param[:default] -%>
Default value: <%= value_string(param[:default]) %>
<% end -%>
<% end -%>
<% end -%>

View File

@ -0,0 +1,24 @@
<% if puppet_classes.length > 0 -%>
## Classes
<% puppet_classes.each do |klassy| -%>
* [`<%= klassy[:name] %>`](#<%= klassy[:link] %>): <%= klassy[:desc] %>
<% end -%>
<% end -%>
<% if puppet_defined_types.length > 0 -%>
## Defined types
<% puppet_defined_types.each do |dtype| -%>
* [`<%= dtype[:name] %>`](#<%= dtype[:link] %>): <%= dtype[:desc] %>
<% end -%>
<% end -%>
<% if puppet_resource_types.length > 0 -%>
## Resource types
<% puppet_resource_types.each do |rtype| -%>
* [`<%= rtype[:name] %>`](#<%= rtype[:link] %>): <%= rtype[:desc] %>
<% end -%>
<% end -%>
<% if puppet_functions.length > 0 -%>
## Functions
<% puppet_functions.each do |func| -%>
* [`<%= func[:name] %>`](#<%= func[:link] %>): <%= func[:desc] %>
<% end -%>
<% end -%>

View File

@ -84,14 +84,14 @@ class PuppetStrings::Yard::CodeObjects::Function < PuppetStrings::Yard::CodeObje
hash[:line] = line
hash[:type] = @function_type.to_s
hash[:signatures] = []
if self.has_tag? :overload
# loop over overloads and append onto the signatures array
self.tags(:overload).each do |o|
hash[:signatures] << { :signature => o.signature, :docstring => PuppetStrings::Json.docstring_to_hash(o.docstring, [:param, :return]) }
hash[:signatures] << { :signature => o.signature, :docstring => PuppetStrings::Json.docstring_to_hash(o.docstring, [:param, :option, :return]) }
end
else
hash[:signatures] << { :signature => self.signature, :docstring => PuppetStrings::Json.docstring_to_hash(docstring, [:param, :return]) }
hash[:signatures] << { :signature => self.signature, :docstring => PuppetStrings::Json.docstring_to_hash(docstring, [:param, :option, :return]) }
end
hash[:docstring] = PuppetStrings::Json.docstring_to_hash(docstring)

View File

@ -22,7 +22,7 @@ class PuppetStrings::Yard::CodeObjects::Type < PuppetStrings::Yard::CodeObjects:
# Represents a resource type parameter.
class Parameter
attr_reader :name, :values, :aliases
attr_accessor :docstring, :isnamevar, :default
attr_accessor :docstring, :isnamevar, :default, :data_type
# Initializes a resource type parameter or property.
# @param [String] name The name of the parameter or property.
@ -31,6 +31,7 @@ class PuppetStrings::Yard::CodeObjects::Type < PuppetStrings::Yard::CodeObjects:
@name = name
@docstring = docstring || ''
@values = []
@data_type = []
@aliases = {}
@isnamevar = false
@default = nil
@ -59,6 +60,7 @@ class PuppetStrings::Yard::CodeObjects::Type < PuppetStrings::Yard::CodeObjects:
hash[:name] = name
hash[:description] = docstring unless docstring.empty?
hash[:values] = values unless values.empty?
hash[:data_type] = data_type unless data_type.empty?
hash[:aliases] = aliases unless aliases.empty?
hash[:isnamevar] = true if isnamevar
hash[:default] = default if default

View File

@ -29,7 +29,7 @@ class PuppetStrings::Yard::Handlers::Ruby::Base < YARD::Handlers::Ruby::Base
source = node.source
if source =~ HEREDOC_START
lines = source.split("\n")
source = lines[1..(lines.last.include?($1) ? -2 : -1)].join("\n") if lines.size > 1
source = lines[1..(lines.last.include?($1[0..-2]) ? -2 : -1)].join("\n") if lines.size > 1
end
source

View File

@ -24,7 +24,7 @@ class PuppetStrings::Yard::Handlers::Ruby::RsapiHandler < PuppetStrings::Yard::H
object = PuppetStrings::Yard::CodeObjects::Type.new(schema['name'])
register object
docstring = schema['docs']
docstring = schema['desc'] || ""
if docstring
register_docstring(object, PuppetStrings::Yard::Util.scrub_string(docstring.to_s), nil)
else
@ -49,7 +49,7 @@ class PuppetStrings::Yard::Handlers::Ruby::RsapiHandler < PuppetStrings::Yard::H
# check that the params of the register_type call are key/value pairs.
def kv_arg_list?(params)
params.type == :list && params.children.count > 0 && params.children.first.type == :list && params.children.first.children.count > 0 && statement.parameters.children.first.children.first.type == :assoc
params.type == :list && params.children.count > 0 && params.children.first.type == :list && params.children.first.children.count > 0 && statement.parameters.children.first.children.first.type == :assoc
end
def extract_schema
@ -134,7 +134,7 @@ class PuppetStrings::Yard::Handlers::Ruby::RsapiHandler < PuppetStrings::Yard::H
end
def set_values(definition, object)
object.add(definition['type']) if definition.key? 'type'
object.data_type = definition['type'] if definition.key? 'type'
object.default = definition['default'] if definition.key? 'default'
object.isnamevar = definition.key?('behaviour') && definition['behaviour'] == 'namevar'
end

View File

@ -7,15 +7,21 @@ Puppet::Face.define(:strings, '0.0.1') do
action(:generate) do
default
option '--emit-json-stdout' do
summary 'Print JSON representation of the documentation to stdout.'
option '--format OUTPUT_FORMAT' do
summary 'Designate output format, JSON or markdown.'
end
option '--emit-json FILE' do
summary 'Write JSON representation of the documentation to the given file.'
option '--out PATH' do
summary 'Write selected format to PATH. If no path is designated, strings prints to STDOUT.'
end
option '--markup FORMAT' do
summary "The markup format to use for docstring text (defaults to 'markdown')."
end
option '--emit-json-stdout' do
summary 'DEPRECATED: Print JSON representation of the documentation to stdout.'
end
option '--emit-json PATH' do
summary 'DEPRECATED: Write JSON representation of the documentation to the given file.'
end
summary 'Generate documentation from files.'
arguments '[[search_pattern] ...]'
@ -94,11 +100,26 @@ Puppet::Face.define(:strings, '0.0.1') do
generate_options[:yard_args] = yard_args unless yard_args.empty?
if options
if options[:emit_json]
$stderr.puts "WARNING: '--emit-json PATH' is deprecated. Use '--format json --out PATH' instead."
end
if options[:emit_json_stdout]
$stderr.puts "WARNING: '--emit-json-stdout' is deprecated. Use '--format json' instead."
end
markup = options[:markup]
generate_options[:markup] = markup if markup
json_file = options[:emit_json]
generate_options[:json] = json_file if json_file
generate_options[:json] = nil if options[:emit_json_stdout]
generate_options[:path] = options[:out] if options[:out]
generate_options[:stdout] = options[:stdout]
format = options[:format]
if format
if format.casecmp('markdown').zero?
generate_options[:markdown] = true
elsif format.casecmp('json').zero? || options[:emit_json] || options[:emit_json_stdout]
generate_options[:json] = true
else
raise RuntimeError, "Invalid format #{options[:format]}. Please select 'json' or 'markdown'."
end
end
end
generate_options
end

View File

@ -37,18 +37,18 @@ describe 'Emitting JSON' do
]
}
it 'should emit JSON to stdout when using the --emit-json-stdout option' do
it 'should emit JSON to stdout when using --format json and --stdout' do
test_module_path = get_test_module_path(master, /Module test/)
on master, puppet('strings', 'generate', '--emit-json-stdout', "#{test_module_path}/lib/puppet/parser/functions/function3x.rb") do
on master, puppet('strings', 'generate', '--format json', "#{test_module_path}/lib/puppet/parser/functions/function3x.rb") do
output = stdout.chomp
expect(JSON.parse(output)).to eq(expected)
end
end
it 'should write JSON to a file when using the --emit-json option' do
it 'should write JSON to a file when using --format json and --out' do
test_module_path = get_test_module_path(master, /Module test/)
tmpfile = master.tmpfile('json_output.json')
on master, puppet('strings', 'generate', "--emit-json #{tmpfile}", "#{test_module_path}/lib/puppet/parser/functions/function3x.rb")
on master, puppet('strings', 'generate', '--format json', "--out #{tmpfile}", "#{test_module_path}/lib/puppet/parser/functions/function3x.rb")
output = read_file_on(master, tmpfile)
expect(JSON.parse(output)).to eq(expected)
end

View File

@ -0,0 +1,49 @@
require 'spec_helper_acceptance'
require 'util'
include PuppetStrings::Acceptance::Util
describe 'Generating Markdown' do
expected = <<-EOF
# Reference
## Classes
* [`test`](#test): This class exists to serve as fixture data for testing the puppet strings face
## Classes
### test
#### Examples
```puppet
class { "test": }
```
#### Parameters
##### `package_name`
The name of the package
##### `service_name`
The name of the service
EOF
it 'should render Markdown to stdout when using --format markdown and --stdout' do
test_module_path = get_test_module_path(master, /Module test/)
on master, puppet('strings', 'generate', '--format markdown', "#{test_module_path}/manifests/init.pp") do
output = stdout.chomp
expect(JSON.parse(output)).to eq(expected)
end
end
it 'should write Markdown to a file when using --format markdown and --out' do
test_module_path = get_test_module_path(master, /Module test/)
tmpfile = master.tmpfile('md_output.md')
on master, puppet('strings', 'generate', '--format markdown', "--out #{tmpfile}", "#{test_module_path}/manifests/init.pp")
output = read_file_on(master, tmpfile)
expect(JSON.parse(output)).to eq(expected)
end
end

View File

@ -81,6 +81,49 @@
}
],
"resource_types": [
{
"name": "apt_key",
"file": "(stdin)",
"line": 92,
"docstring": {
"text": "This type provides Puppet with the capabilities to manage GPG keys needed\nby apt to perform package validation. Apt has it's own GPG keyring that can\nbe manipulated through the `apt-key` command.\n**Autorequires**:\nIf Puppet is given the location of a key file which looks like an absolute\npath this type will autorequire that file.",
"tags": [
{
"tag_name": "summary",
"text": "Example resource type using the new API."
},
{
"tag_name": "raise",
"text": "SomeError"
},
{
"tag_name": "example",
"text": "apt_key { '6F6B15509CF8E59E6E469F327F438280EF8D349F':\n source => 'http://apt.puppetlabs.com/pubkey.gpg'\n}",
"name": "here's an example"
}
]
},
"properties": [
{
"name": "ensure",
"description": "Whether this apt key should be present or absent on the target system.",
"data_type": "Enum[present, absent]"
},
{
"name": "created",
"description": "Date the key was created, in ISO format.",
"data_type": "String"
}
],
"parameters": [
{
"name": "id",
"description": "The ID of the key you want to manage.",
"data_type": "Variant[Pattern[/A(0x)?[0-9a-fA-F]{8}Z/], Pattern[/A(0x)?[0-9a-fA-F]{16}Z/], Pattern[/A(0x)?[0-9a-fA-F]{40}Z/]]",
"isnamevar": true
}
]
},
{
"name": "database",
"file": "(stdin)",

368
spec/fixtures/unit/markdown/output.md vendored Normal file
View File

@ -0,0 +1,368 @@
# Reference
## Classes
* [`klass`](#klass): A simple class.
## Defined types
* [`klass::dt`](#klassdt): A simple defined type.
## Resource types
* [`apt_key`](#apt_key): Example resource type using the new API.
* [`database`](#database): An example database server type.
## Functions
* [`func`](#func): A simple Puppet function.
* [`func3x`](#func3x): Documentation for an example 3.x function.
* [`func4x`](#func4x): An example 4.x function.
* [`func4x_1`](#func4x_1): An example 4.x function with only one signature.
## Classes
### klass
* **Since** 1.0.0
* **See also**
www.puppet.com
#### Examples
##### This is an example
```puppet
class { 'klass':
param1 => 1,
param3 => 'foo',
}
```
##### This is another example
```puppet
class { 'klass':
param1 => 1,
param3 => 'foo',
}
```
#### Parameters
The following parameters are available in the `klass` class.
##### `param1`
Data type: `Integer`
First param.
Default value: 1
##### `param2`
Data type: `Any`
Second param.
Options:
* **:opt1** `String`: something about opt1
* **:opt2** `Hash`: a hash of stuff
Default value: `undef`
##### `param3`
Data type: `String`
Third param.
Default value: 'hi'
## Defined types
### klass::dt
* **Since** 1.1.0
* **See also**
www.puppet.com
#### Examples
##### Here's an example of this type:
```puppet
klass::dt { 'foo':
param1 => 33,
param4 => false,
}
```
#### Parameters
The following parameters are available in the `klass::dt` defined type.
##### `param1`
Data type: `Integer`
First param.
Default value: 44
##### `param2`
Data type: `Any`
Second param.
Options:
* **:opt1** `String`: something about opt1
* **:opt2** `Hash`: a hash of stuff
##### `param3`
Data type: `String`
Third param.
Default value: 'hi'
##### `param4`
Data type: `Boolean`
Fourth param.
Default value: `true`
## Resource types
### apt_key
This type provides Puppet with the capabilities to manage GPG keys needed
by apt to perform package validation. Apt has it's own GPG keyring that can
be manipulated through the `apt-key` command.
**Autorequires**:
If Puppet is given the location of a key file which looks like an absolute
path this type will autorequire that file.
#### Examples
##### here's an example
```puppet
apt_key { '6F6B15509CF8E59E6E469F327F438280EF8D349F':
source => 'http://apt.puppetlabs.com/pubkey.gpg'
}
```
#### Properties
The following properties are available in the `apt_key` type.
##### `ensure`
Data type: `Enum[present, absent]`
Whether this apt key should be present or absent on the target system.
##### `created`
Data type: `String`
Date the key was created, in ISO format.
#### Parameters
The following parameters are available in the `apt_key` type.
##### `id`
namevar
Data type: `Variant[Pattern[/A(0x)?[0-9a-fA-F]{8}Z/], Pattern[/A(0x)?[0-9a-fA-F]{16}Z/], Pattern[/A(0x)?[0-9a-fA-F]{40}Z/]]`
_*this data type contains a regex that may not be accurately reflected in generated documentation_
The ID of the key you want to manage.
### database
An example database server type.
#### Examples
##### here's an example
```puppet
database { 'foo':
address => 'qux.baz.bar',
}
```
#### Properties
The following properties are available in the `database` type.
##### `ensure`
Valid values: present, absent, up, down
Aliases: "up"=>"present", "down"=>"absent"
What state the database should be in.
Default value: up
##### `file`
The database file to use.
##### `log_level`
Valid values: debug, warn, error
The log level to use.
Default value: warn
#### Parameters
The following parameters are available in the `database` type.
##### `address`
namevar
The database server name.
##### `encryption_key`
The encryption key to use.
##### `encrypt`
Valid values: `true`, `false`, yes, no
Whether or not to encrypt the database.
Default value: `false`
## Functions
### func
Type: Puppet Language
#### `func(Integer $param1, Any $param2, String $param3 = hi)`
A simple Puppet function.
Returns: `Undef` Returns nothing.
Raises:
* `SomeError` this is some error
##### `param1`
Data type: `Integer`
First param.
##### `param2`
Data type: `Any`
Second param.
##### `param3`
Data type: `String`
Third param.
Options:
* **:param3opt** `Array`: Something about this option
### func3x
Type: Ruby 3.x API
#### `func3x(String $param1, Integer $param2)`
Documentation for an example 3.x function.
Returns: `Undef`
##### `param1`
Data type: `String`
The first parameter.
##### `param2`
Data type: `Integer`
The second parameter.
### func4x
Type: Ruby 4.x API
#### `func4x(Integer $param1, Any $param2, Optional[Array[String]] $param3)`
An overview for the first overload.
Returns: `Undef` Returns nothing.
##### `param1`
Data type: `Integer`
The first parameter.
##### `param2`
Data type: `Any`
The second parameter.
Options:
* **:option** `String`: an option
* **:option2** `String`: another option
##### `param3`
Data type: `Optional[Array[String]]`
The third parameter.
#### `func4x(Boolean $param, Callable &$block)`
An overview for the second overload.
Returns: `String` Returns a string.
##### `param`
Data type: `Boolean`
The first parameter.
##### `&block`
Data type: `Callable`
The block parameter.
### func4x_1
Type: Ruby 4.x API
#### `func4x_1(Integer $param1)`
An example 4.x function with only one signature.
Returns: `Undef` Returns nothing.
##### `param1`
Data type: `Integer`
The first parameter.

View File

@ -19,6 +19,8 @@ require 'mocha'
require 'rspec'
require 'puppet/version'
require 'puppet-strings'
require 'puppet-strings/markdown'
require 'puppet-strings/markdown/base'
require 'puppet-strings/yard'
# Explicitly set up YARD once

View File

@ -123,6 +123,46 @@ Puppet::Type.newtype(:database) do
defaultto 'warn'
end
end
Puppet::ResourceApi.register_type(
name: 'apt_key',
desc: <<-EOS,
@summary Example resource type using the new API.
@raise SomeError
This type provides Puppet with the capabilities to manage GPG keys needed
by apt to perform package validation. Apt has it's own GPG keyring that can
be manipulated through the `apt-key` command.
@example here's an example
apt_key { '6F6B15509CF8E59E6E469F327F438280EF8D349F':
source => 'http://apt.puppetlabs.com/pubkey.gpg'
}
**Autorequires**:
If Puppet is given the location of a key file which looks like an absolute
path this type will autorequire that file.
EOS
attributes: {
ensure: {
type: 'Enum[present, absent]',
desc: 'Whether this apt key should be present or absent on the target system.'
},
id: {
type: 'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]',
behaviour: :namevar,
desc: 'The ID of the key you want to manage.',
},
# ...
created: {
type: 'String',
behaviour: :read_only,
desc: 'Date the key was created, in ISO format.',
},
},
autorequires: {
file: '$source', # will evaluate to the value of the `source` attribute
package: 'apt',
},
)
SOURCE
end

View File

@ -0,0 +1,133 @@
require 'spec_helper'
describe PuppetStrings::Markdown::Base do
context 'basic class' do
before :each do
YARD::Parser::SourceParser.parse_string(<<-SOURCE, :puppet)
# An overview
# @summary A simple class.
# @param param1 First param.
# @param param2 Second param.
# @param param3 Third param.
class klass(Integer $param1, $param2, String $param3 = hi) inherits foo::bar {
}
SOURCE
end
let(:reg) { YARD::Registry.all(:puppet_class).sort_by!(&:name).map!(&:to_hash)[0] }
let(:component) { PuppetStrings::Markdown::Base.new(reg, 'class') }
describe '#name' do
it 'returns the expected name' do
expect(component.name).to eq 'klass'
end
end
[ 'examples',
'see',
'since',
'return_val',
'return_type',].each do |method|
describe "##{method}" do
it 'returns nil' do
expect(component.method(method.to_sym).call).to be_nil
end
end
end
describe '#params' do
it 'returns the expected params' do
expect(component.params.size).to eq 3
end
end
describe '#summary' do
it 'returns the expected summary' do
expect(component.summary).to eq 'A simple class.'
end
end
describe '#toc_info' do
let(:toc) { component.toc_info }
it 'returns a hash' do
expect(toc).to be_instance_of Hash
end
it 'prefers the summary for :desc' do
expect(toc[:desc]).to eq 'A simple class.'
end
end
end
context 'less basic class' do
before :each do
YARD::Parser::SourceParser.parse_string(<<-SOURCE, :puppet)
# An overview
# It's a longer overview
# Ya know?
# @example A simple example.
# class { 'klass::yeah':
# param1 => 1,
# }
# @param param1 First param.
# @param param2 Second param.
# @param param3 Third param.
class klass::yeah(
Integer $param1,
$param2,
String $param3 = hi
) inherits foo::bar {
}
SOURCE
end
let(:reg) { YARD::Registry.all(:puppet_class).sort_by!(&:name).map!(&:to_hash)[0] }
let(:component) { PuppetStrings::Markdown::Base.new(reg, 'class') }
describe '#name' do
it 'returns the expected name' do
expect(component.name).to eq 'klass::yeah'
end
end
['summary',
'see',
'since',
'return_val',
'return_type'].each do |method|
describe "##{method}" do
it 'returns nil' do
expect(component.method(method.to_sym).call).to be_nil
end
end
end
describe '#examples' do
it 'should return one example' do
expect(component.examples.size).to eq 1
end
end
describe '#params' do
it 'returns the expected params' do
expect(component.params.size).to eq 3
end
end
describe '#toc_info' do
let(:toc) { component.toc_info }
it 'returns a hash' do
expect(toc).to be_instance_of Hash
end
it 'uses overview for :desc in absence of summary' do
expect(toc[:desc]).to eq 'An overview. It\'s a longer overview. Ya know?'
end
end
describe '#link' do
it 'returns a valid link' do
expect(component.link).to eq 'klassyeah'
end
end
end
end

View File

@ -0,0 +1,244 @@
require 'spec_helper'
require 'puppet-strings/markdown'
require 'puppet-strings/markdown/table_of_contents'
require 'tempfile'
describe PuppetStrings::Markdown do
before :each do
# Populate the YARD registry with both Puppet and Ruby source
YARD::Parser::SourceParser.parse_string(<<-SOURCE, :puppet)
# An overview for a simple class.
# @summary A simple class.
# @since 1.0.0
# @see www.puppet.com
# @example This is an example
# class { 'klass':
# param1 => 1,
# param3 => 'foo',
# }
# @example This is another example
# class { 'klass':
# param1 => 1,
# param3 => 'foo',
# }
# @raise SomeError
# @param param1 First param.
# @param param2 Second param.
# @option param2 [String] :opt1 something about opt1
# @option param2 [Hash] :opt2 a hash of stuff
# @param param3 Third param.
#
class klass (
Integer $param1 = 1,
$param2 = undef,
String $param3 = 'hi'
) inherits foo::bar {
}
# An overview for a simple defined type.
# @summary A simple defined type.
# @since 1.1.0
# @see www.puppet.com
# @example Here's an example of this type:
# klass::dt { 'foo':
# param1 => 33,
# param4 => false,
# }
# @return shouldn't return squat
# @raise SomeError
# @param param1 First param.
# @param param2 Second param.
# @option param2 [String] :opt1 something about opt1
# @option param2 [Hash] :opt2 a hash of stuff
# @param param3 Third param.
# @param param4 Fourth param.
define klass::dt (
Integer $param1 = 44,
$param2,
String $param3 = 'hi',
Boolean $param4 = true
) {
}
SOURCE
YARD::Parser::SourceParser.parse_string(<<-SOURCE, :puppet)
# A simple Puppet function.
# @param param1 First param.
# @param param2 Second param.
# @param param3 Third param.
# @option param3 [Array] :param3opt Something about this option
# @raise SomeError this is some error
# @return [Undef] Returns nothing.
function func(Integer $param1, $param2, String $param3 = hi) {
}
SOURCE
YARD::Parser::SourceParser.parse_string(<<-SOURCE, :ruby)
# An example 4.x function.
Puppet::Functions.create_function(:func4x) do
# An overview for the first overload.
# @raise SomeError this is some error
# @param param1 The first parameter.
# @param param2 The second parameter.
# @option param2 [String] :option an option
# @option param2 [String] :option2 another option
# @param param3 The third parameter.
# @return Returns nothing.
dispatch :foo do
param 'Integer', :param1
param 'Any', :param2
optional_param 'Array[String]', :param3
return_type 'Undef'
end
# An overview for the second overload.
# @param param The first parameter.
# @param block The block parameter.
# @return Returns a string.
dispatch :other do
param 'Boolean', :param
block_param
return_type 'String'
end
end
# An example 4.x function with only one signature.
Puppet::Functions.create_function(:func4x_1) do
# @param param1 The first parameter.
# @return [Undef] Returns nothing.
dispatch :foobarbaz do
param 'Integer', :param1
end
end
# An example 3.x function
Puppet::Parser::Functions.newfunction(:func3x, doc: <<-DOC
Documentation for an example 3.x function.
@param param1 [String] The first parameter.
@param param2 [Integer] The second parameter.
@return [Undef]
@example Calling the function.
func3x('hi', 10)
DOC
) do |*args|
#...
end
Puppet::Type.type(:database).provide :linux do
desc 'An example provider on Linux.'
confine kernel: 'Linux'
confine osfamily: 'RedHat'
defaultfor :kernel => 'Linux'
defaultfor :osfamily => 'RedHat', :operatingsystemmajrelease => '7'
has_feature :implements_some_feature
has_feature :some_other_feature
commands foo: '/usr/bin/foo'
end
Puppet::Type.newtype(:database) do
desc <<-DESC
An example database server type.
@option opts :foo bar
@raise SomeError
@example here's an example
database { 'foo':
address => 'qux.baz.bar',
}
DESC
feature :encryption, 'The provider supports encryption.', methods: [:encrypt]
ensurable do
desc 'What state the database should be in.'
defaultvalues
aliasvalue(:up, :present)
aliasvalue(:down, :absent)
defaultto :up
end
newparam(:address) do
isnamevar
desc 'The database server name.'
end
newparam(:encryption_key, required_features: :encryption) do
desc 'The encryption key to use.'
end
newparam(:encrypt, :parent => Puppet::Parameter::Boolean) do
desc 'Whether or not to encrypt the database.'
defaultto false
end
newproperty(:file) do
desc 'The database file to use.'
end
newproperty(:log_level) do
desc 'The log level to use.'
newvalue(:debug)
newvalue(:warn)
newvalue(:error)
defaultto 'warn'
end
end
Puppet::ResourceApi.register_type(
name: 'apt_key',
desc: <<-EOS,
@summary Example resource type using the new API.
@raise SomeError
This type provides Puppet with the capabilities to manage GPG keys needed
by apt to perform package validation. Apt has it's own GPG keyring that can
be manipulated through the `apt-key` command.
@example here's an example
apt_key { '6F6B15509CF8E59E6E469F327F438280EF8D349F':
source => 'http://apt.puppetlabs.com/pubkey.gpg'
}
**Autorequires**:
If Puppet is given the location of a key file which looks like an absolute
path this type will autorequire that file.
EOS
attributes: {
ensure: {
type: 'Enum[present, absent]',
desc: 'Whether this apt key should be present or absent on the target system.'
},
id: {
type: 'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]',
behaviour: :namevar,
desc: 'The ID of the key you want to manage.',
},
# ...
created: {
type: 'String',
behaviour: :read_only,
desc: 'Date the key was created, in ISO format.',
},
},
autorequires: {
file: '$source', # will evaluate to the value of the `source` attribute
package: 'apt',
},
)
SOURCE
end
let(:filename) { 'output.md' }
let(:baseline_path) { File.join(File.dirname(__FILE__), "../../fixtures/unit/markdown/#{filename}") }
let(:baseline) { File.read(baseline_path) }
describe 'rendering markdown to a file' do
it 'should output the expected markdown content' do
Tempfile.open('md') do |file|
PuppetStrings::Markdown.render(file.path)
expect(File.read(file.path)).to eq(baseline)
end
end
end
describe 'rendering markdown to stdout' do
it 'should output the expected markdown content' do
expect{ PuppetStrings::Markdown.render }.to output(baseline).to_stdout
end
end
end