Skip to content

Commit

Permalink
Merge pull request #106 from nebolsin/feature/markdown-docs
Browse files Browse the repository at this point in the history
Additional API schema documentation using markdown files
  • Loading branch information
razum2um committed Jun 30, 2015
2 parents 2a77287 + 430fd29 commit feacf6f
Show file tree
Hide file tree
Showing 23 changed files with 414 additions and 345 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ The generation of live-documentation is pretty simple:

For different document root or serving URL prefix use `-o` and `-u` options accordingly.

If you want to provide additional documentation for your API (and you probably should),
you can use separate Markdown files in the schema folder. To generate documentation
stubs for the current schema:

bin/lurker init_docs # generate documentation stubs for the current schema


Let's run your `rails s` and visit [http://localhost:3000/lurker/](http://localhost:3000/lurker/)
(or see [demo][demo_app2] for example)

Expand Down
10 changes: 9 additions & 1 deletion lib/lurker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ def self.service_path
@service_path || DEFAULT_SERVICE_PATH
end

def self.valid_service_path?
Dir.exist? service_path
end

def self.service
@service ||= Lurker::Service.new(service_path)
end

def self.decide_success_with(&block)
@success_block = block
end
Expand Down Expand Up @@ -65,7 +73,7 @@ class UndocumentedResponseCode < ValidationError; end
require 'lurker/presenters/response_code_presenter'
require 'lurker/json'
require 'lurker/json/reader'
require 'lurker/json/writter'
require 'lurker/json/writer'
require 'lurker/json/orderer'
require 'lurker/json/parser'
require 'lurker/json/parser/expertise'
Expand Down
278 changes: 148 additions & 130 deletions lib/lurker/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,148 +5,134 @@
require 'active_support/inflector'

module Lurker
BUNDLED_TEMPLATES_PATH = Pathname.new('../templates').expand_path(__FILE__)
BUNDLED_ASSETS_PATH = Pathname.new('../templates/public').expand_path(__FILE__)

# A Thor::Error to be thrown when an lurker directory is not found
class NotFound < Thor::Error; end

# A Thor definition for an lurker to HTML conversion operation
class Cli < Thor
include Thor::Actions

attr_accessor :origin_path, :content
attr_accessor :content

def self.templates_root
options[:template].present? ? Pathname.new(options[:templates]).expand_path : Lurker::BUNDLED_TEMPLATES_PATH
end

def self.source_root
File.expand_path("../templates", __FILE__)
def self.assets_root
options[:assets].present? ? Pathname.new(options[:assets]).expand_path : Lurker::BUNDLED_ASSETS_PATH
end

def self.precompiled_static_root
File.expand_path("../templates/public", __FILE__)
source_root(templates_root)

desc 'init_docs [LURKER_PATH]', 'Create documentation stubs for service and endpoints'

def init_docs(lurker_path=Lurker::DEFAULT_SERVICE_PATH)
say_status nil, 'Creating documentation stubs'

setup_schema_root! lurker_path

schemas = Lurker.service.endpoints.map(&:schema) << Lurker.service.schema
schemas.each do |schema|
rel_path = Pathname.new(schema.documentation_uri).relative_path_from(Pathname.getwd)
template 'documentation.md.tt', schema.documentation_uri, skip: true, path: rel_path
end
end

desc "convert [LURKER_PATH]", "Convert lurker to HTML"
method_option :rails, :type => :boolean, :desc => "Includes Rails environment"
method_option :exclude, :aliases => "-e", :desc => "Select endpoints by given regexp, if NOT matching prefix"
method_option :select, :aliases => "-s", :desc => "Select endpoints by given regexp, matching prefix"
method_option :output, :aliases => "-o", :desc => "Output path", :default => "public"
method_option :url_base_path, :aliases => "-u", :desc => "URL base path", :default => Lurker::DEFAULT_URL_BASE
method_option :format, :aliases => "-f", :desc => "Format in html or pdf, defaults to html", :default => "html"
method_option :templates, :aliases => "-t", :desc => "Template overrides path"
method_option :content, :aliases => "-c", :desc => "Content to be rendered into html-docs main page"
desc 'convert [LURKER_PATH]', 'Convert lurker to HTML'
option :rails, type: :boolean, desc: 'Includes Rails environment'
option :exclude, aliases: '-e', desc: 'Select endpoints by given regexp, if NOT matching prefix'
option :select, aliases: '-s', desc: 'Select endpoints by given regexp, matching prefix'
option :output, aliases: '-o', desc: 'Output path', default: 'public'
option :url_base_path, aliases: '-u', desc: 'URL base path', default: Lurker::DEFAULT_URL_BASE
option :templates, aliases: '-t', desc: 'Template overrides path'
option :assets, aliases: '-a', desc: 'Assets overrides path'
option :format, aliases: '-f', desc: 'Format in html or pdf, defaults to html', default: 'html'
option :content, aliases: '-c', desc: 'Content to be rendered into html-docs main page'

def convert(lurker_path=Lurker::DEFAULT_SERVICE_PATH)
say_status nil, "Converting lurker to #{options[:format]}"

self.content = get_content(options[:content])
self.origin_path = File.expand_path(lurker_path)
raise Lurker::NotFound.new(origin_path) unless has_valid_origin?
say_status :using, lurker_path

FileUtils.mkdir_p(output_path)
say_status :inside, output_path
setup_schema_root! lurker_path
require "#{Dir.pwd}/config/environment" if options[:rails]

if options[:rails]
require "#{Dir.pwd}/config/environment"
# for backward compatibility
if options[:content]
Lurker.service.documentation = open(File.expand_path(options[:content])).read
end

if options[:format] == 'pdf'
convert_to_pdf
else
convert_to_html
setup_rendering_engine!

inside(output_path) do
say_status :inside, output_path
prepare_assets!
if options[:format] == 'pdf'
convert_to_pdf
else
convert_to_html
end
cleanup_assets!
end
end

no_tasks do
def convert_to_pdf
Lurker.safe_require('pdfkit')
css = File.expand_path('application.css', self.class.precompiled_static_root)
inside(output_path) do
service_presenters.each do |service_presenter|
html = "<html><body>"
html << service_presenter.to_html(layout: false)
service_presenter.endpoints.each do |endpoint_prefix_group|
endpoint_prefix_group.each do |endpoint_presenter|
html << endpoint_presenter.to_html(layout: false)
end
end
html << "</body></html>"
kit = PDFKit.new(html, :page_size => 'Letter')
kit.stylesheets << css
url_name = ActiveSupport::Inflector.parameterize(service_presenter.name, '_')
create_file("#{url_name}.pdf", kit.to_pdf, force: true)
end
end
kit = PDFKit.new(service_presenter.to_print)
kit.stylesheets << assets['application.css']
create_file "#{service_presenter.url_name}.pdf", kit.to_pdf, force: true
end

def convert_to_html
inside(output_path) do
# js, css, fonts
static = []
Dir["#{self.class.precompiled_static_root}/*"].each do |fname|
if match = fname.match(/application\.(js|css)$/)
sha1 = Digest::SHA1.hexdigest(open(fname).read)
html_options.merge! match[1] => sha1
static << (new_name = "application-#{sha1}.#{match[1]}")
FileUtils.cp_r fname, new_name
spawn "cat #{new_name} | gzip -9 > #{new_name}.gz"
else
FileUtils.cp_r fname, Pathname.new(fname).basename.to_s
end
end

service_presenters.each do |service_presenter|
create_file("index.html", service_presenter.to_html, force: true)

service_presenter.endpoints.each do |endpoint_prefix_group|
endpoint_prefix_group.each do |endpoint_presenter|
create_file(endpoint_presenter.relative_path, endpoint_presenter.to_html, force: true)
end
end
end
create_file 'index.html', service_presenter.to_html, force: true

# cleanup
Dir.glob("*.js").each do |fname|
FileUtils.rm fname unless static.include? fname
end
Dir.glob("*.css").each do |fname|
FileUtils.rm fname unless static.include? fname
end
Dir.glob("*.gz").each do |fname|
FileUtils.rm fname unless static.include? fname.sub(/\.gz/, '')
service_presenter.endpoints.each do |endpoint_prefix_group|
endpoint_prefix_group.each do |endpoint_presenter|
create_file(endpoint_presenter.relative_path, endpoint_presenter.to_html, force: true)
end
end
end

def output_path
"#{output_prefix}/#{url_base_path}"
def setup_schema_root!(path)
Lurker.service_path = File.expand_path(path)
raise Lurker::NotFound.new(Lurker.service_path) unless Lurker.valid_service_path?
say_status :using, path
end

def output_prefix
if explicit = options[:output]
explicit.sub(/\/?#{url_base_path}\/?$/, '')
elsif File.exists? "public"
"public"
else
raise "Please, run it from `Rails.root` or pass `-o` option"
end
def setup_rendering_engine!
I18n.config.enforce_available_locales = true
Lurker::RenderingController.prepend_view_path templates_root
Lurker::RenderingController.config.assets_dir = assets_root
end

def template_path
@template_path ||=
if options[:templates]
File.expand_path(options[:templates])
else
File.expand_path("../templates", origin_path)
end
def prepare_assets!
directory assets_root, '.', exclude_pattern: /application\.(js|css)$/
digest_asset!('application.js')
digest_asset!('application.css')
end

def has_valid_origin?
origin.directory?
def cleanup_assets!
actual = assets.values
Dir.glob('*.{js,css,gz}').each do |fname|
remove_file fname unless actual.include? fname.sub(/\.gz/, '')
end
end

def service_presenters
@service_presenters ||= services.map do |service|
Lurker::ServicePresenter.new(service, html_options, &filtering_block)
def digest_asset!(name)
if (asset_path = assets_root + name).exist?
digest_path = asset_digest_path(asset_path).basename
assets[asset_path.basename.to_s] = digest_path.to_s
copy_file asset_path, digest_path, skip: true
spawn "cat #{digest_path} | gzip -9 > #{digest_path}.gz"
end
end

def service_presenter
@service_presenter ||= Lurker::ServicePresenter.new(Lurker.service, html_options, &filtering_block)
end

def filtering_block
if options['select'].present?
select = /#{options['select']}/
Expand All @@ -168,29 +154,80 @@ def filtering_block

def html_options
@html_options ||= {
:static_html => true,
:url_base_path => url_base_path.prepend('/'),
:template_directory => template_path,
:html_directory => output_path,
:content => self.content,
:footer => (`git rev-parse --short HEAD`.to_s.strip rescue ""),
:lurker => gem_info
static_html: true,
url_base_path: url_base_path.prepend('/'),
template_directory: templates_root,
assets_directory: assets_root,
assets: assets,
html_directory: output_path,
footer: footer,
lurker: gem_info
}
end
end

private

def output_path
"#{output_prefix}/#{url_base_path}"
end

def output_prefix
if explicit = options[:output]
explicit.sub(/\/?#{url_base_path}\/?$/, '')
elsif File.exists? 'public'
'public'
else
raise 'Please, run it from `Rails.root` or pass `-o` option'
end
end

def url_base_path
options[:url_base_path].presence.try(:strip).try(:sub, /^\/+/, '') || Lurker::DEFAULT_URL_BASE
end

def assets
@assets ||= {}
end

def assets_root
Lurker::Cli.assets_root
end

def templates_root
Lurker::Cli.templates_root
end

def asset_logical_path(path)
path = Pathname.new(path) unless path.is_a? Pathname
path.sub %r{-[0-9a-f]{40}\.}, '.'
end

def asset_digest_path(path)
path = Pathname.new(path) unless path.is_a? Pathname
asset_logical_path(path).sub(/\.(\w+)$/) { |ext| "-#{hexdigest(path)}#{ext}" }
end

def hexdigest(path)
Digest::SHA1.hexdigest(open(path).read)
end

def path_extnames(path)
File.basename(path).scan(/\.[^.]+/)
end

def footer
`git rev-parse --short HEAD`.to_s.strip
rescue
''
end

def gem_info
spec = if Bundler.respond_to? :locked_gems
Bundler.locked_gems.specs.select { |s| s.name == 'lurker' } .first # 1.6
else
Bundler.definition.sources.detect { |s| s.specs.map(&:name).include?('lurker') } # 1.3
end
Bundler.locked_gems.specs.select { |s| s.name == 'lurker' }.first # 1.6
else
Bundler.definition.sources.detect { |s| s.specs.map(&:name).include?('lurker') } # 1.3
end

if spec.source.respond_to? :revision, true # bundler 1.3 private
"#{spec.name} (#{spec.source.send(:revision)})"
Expand All @@ -199,26 +236,7 @@ def gem_info
end
rescue => e
puts e
"lurker (unknown)"
end

def get_content(content_fname)
return unless content_fname
content_fname = File.expand_path(content_fname)
if content_fname.ends_with? 'md'
Lurker.safe_require('kramdown')
Kramdown::Document.new(open(content_fname).read).to_html
else
''
end
end

def services
@services ||= [Lurker::Service.new(origin_path)]
end

def origin
Pathname.new(origin_path)
'lurker (unknown)'
end
end
end

0 comments on commit feacf6f

Please sign in to comment.