Making Confluence your storage for technical documentation
Usually, for storing company-specific information, used some kind of documentation portal. We thought, why not to automate process of Confluence Space with pages generated from the codebase? When developers are writing the code, the best practice and LSP support are motivating them to write the documentation for their methods, classes and models. We can gather all of that by utilizing documentation generators and then publish it to a company-wide portal. In this example, let's use Rails application. We also will use RDoc for the parsing and generating of documentation
Prerequisites for documentation generation
- Install the rdoc gem
gem install rdoc --user
- Cover your models with RDoc comments, for example:
# Put here a description of your model/class
class DocumentationRecord < ApplicationRecord
##
# :attr: general_docs
# You need to specify the :attr: keyword in case you don't want the documentation
# to be referred by method, but rather a field
belongs_to :general_docs
##
# :attr: site
# Every documented property should be visible in generated docs
belongs_to :site, optional: true
# You can also generate documentation for the methods of model
# If you indent using two spaces after the comment - you can
# provide an example how to use this method
#
# # call somewhere in your controller like this:
# documentation_record.validate_document
def validate_document()
puts "Validation of document started"
end
end
- Obtain the necessary details/credentials from your Confluence
- You need the
CONFLUENCE_URI
variable, which equal tohttps://your-organization-confluence.com/rest/api
- The
SPACE_KEY
variable, which you can obtain by going to your organization space, where you will be uploading documentation TOKEN
, which has the permission to edit in that Confluence space
- You have to create your own custom RDoc generator.
Every RDoc generator needs to follow their structure, you can use use this code and make adjustments to your own liking. Pay an attention to method "template_path", because it indicates where you will be reading the template used for the generating of HTML for the Confluence.
One of the important notes, is that due to the fact that your generated HTML will be sent as an embedded part
The complete generator code:
require 'uri'
require 'net/http'
require 'rdoc/rdoc'
require "erb"
require 'json'
module RDoc
module Generator
class Confluencer
::RDoc::RDoc.add_generator self
def initialize(store, options)
@store = store
@options = options
end
# Send a GET to the page name located inside the SPACE_KEY
#
# returns: version_number, content_id
def get_confluence_page_version(name)
uri = URI("#{CONFLUENCE_URI}/content?spaceKey=#{SPACE_KEY}&title=#{name}&expand=version")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
json_results = JSON.parse(res.body)['results']
return json_results[0]['version']['number'], json_results[0]['id'] unless json_results.empty?
end
# In case the page already exists in confluence, it's api requires to use "PUT"
# To determine if the page already exists or not, send beforehands a GET to the
# SPACE_KEY for retrieving a version of existing document due to the
# requirements of PUT - to have in JSON field "version", indicating the update
def upload_confluence_page(name, contents, version, content_id)
uri = URI("#{CONFLUENCE_URI}/content#{version.nil? ? "?spaceKey=#{SPACE_KEY}" : "/#{content_id}"}")
version.nil? ? req = Net::HTTP::Post.new(uri) : req = Net::HTTP::Put.new(uri)
req['Authorization'] = "Bearer #{TOKEN}"
version_string = "\"version\": { \"number\": #{version + 1} }," unless version.nil?
req.body = <<-JSON
{
#{version.nil? ? '' : version_string}
"type":"page",
"title":"#{name}",
"space":{
"key":"#{SPACE_KEY}"
},
"body":{
"storage":{
"value": "<ac:structured-macro ac:name=\\\"html\\\" ac:schema-version=\\\"1\\\"><ac:plain-text-body><![CDATA[<div>#{contents}</div>]]></ac:plain-text-body></ac:structured-macro>",
"representation":"storage"
}
}
}
JSON
req.content_type = 'application/json'
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
end
def generate
@classes = @store.all_classes_and_modules.sort
@classes.each do |klass|
template_content = File.read(template_path)
template = ERB.new template_content, trim_mode: '>'
result = template
.result(binding)
.delete("\r\n\\")
.squeeze(' ')
.gsub('"', "'")
version, content_id = get_confluence_page_version(klass.full_name)
upload_confluence_page(klass.full_name, result, version, content_id)
end
end
def class_dir; nil; end
def file_dir; nil; end
private
def template_path
'../template.html.erb'
end
end
end
end
<style>
pre {
background: #f8f8f8;
}
.ruby-comment {
display: block;
color: #777;
}
</style>
<div id='generator-body'>
<h1> <%= klass.type %> <%= klass.full_name %></h1>
<p><%= klass.description %></p>
<% klass.each_section do |section, constants, attributes| %>
<% if section.title %>## <%= section.title.strip %> <% end %>
<% if section.comment %> <%= section.description %><% end%>
<% unless klass.constants.empty? %>
<h2> Constants </h2>
<table>
<thead>
<tr><th>Name</th><th>Description</th></tr>
</thead>
<tbody>
<% klass.constants.each do |const| %>
<tr>
<td><%= const.name %></td>
<td><% unless const.description.empty? %> <%= const.description %> <%else%> Not documented <% end %> </td>
</tr>
<% end %>
<% end %>
<% unless attributes&.empty? %>
<h2> Attributes </h2>
<% attributes.each do |attr| %>
<h3> <%= attr.name %> </h3>
<% if attr.comment %> <%= attr.description %> <%else%> Not documented <% end %>
<% end %>
<% end %>
<% klass.methods_by_type(section).each do |type, visibilities| %>
<% next if visibilities.empty? %>
<% visibilities.each do |visibility, methods| %>
<% next if methods.empty? %>
<h2><%= visibility.capitalize %> <%= type.capitalize %> Methods </h2>
<% methods.each do |method|%>
<h3><%= method.name %><%= method.param_seq %> </h3>
<% if method.comment %> <%= method.description %> <% else %> Not documented <%end%>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
- You have to also invoke this generator with the files for which you want to have documentation
options = RDoc::Options.new
options.exclude = Regexp.union(options.exclude, /node_modules/)
options.setup_generator 'confluencer'
options.files = ['app']
rdoc = RDoc::RDoc.new
rdoc.document options
Photo by Annie Spratt on Unsplash