betterdocs 0.2.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 +7 -0
- data/.codeclimate.yml +5 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/COPYING +202 -0
- data/Gemfile +8 -0
- data/LICENSE +202 -0
- data/README.md +124 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/betterdocs.gemspec +48 -0
- data/lib/betterdocs.rb +27 -0
- data/lib/betterdocs/controller_collector.rb +50 -0
- data/lib/betterdocs/dsl.rb +9 -0
- data/lib/betterdocs/dsl/common.rb +26 -0
- data/lib/betterdocs/dsl/controller.rb +9 -0
- data/lib/betterdocs/dsl/controller/action.rb +126 -0
- data/lib/betterdocs/dsl/controller/action/param.rb +25 -0
- data/lib/betterdocs/dsl/controller/action/response.rb +47 -0
- data/lib/betterdocs/dsl/controller/controller.rb +31 -0
- data/lib/betterdocs/dsl/controller/controller_base.rb +21 -0
- data/lib/betterdocs/dsl/json_params.rb +8 -0
- data/lib/betterdocs/dsl/json_params/param.rb +31 -0
- data/lib/betterdocs/dsl/json_type_mapper.rb +27 -0
- data/lib/betterdocs/dsl/naming.rb +32 -0
- data/lib/betterdocs/dsl/representer.rb +29 -0
- data/lib/betterdocs/dsl/result.rb +10 -0
- data/lib/betterdocs/dsl/result/collection_property.rb +9 -0
- data/lib/betterdocs/dsl/result/link.rb +37 -0
- data/lib/betterdocs/dsl/result/property.rb +53 -0
- data/lib/betterdocs/generator/config_shortcuts.rb +28 -0
- data/lib/betterdocs/generator/markdown.rb +151 -0
- data/lib/betterdocs/generator/markdown/templates/README.md.erb +9 -0
- data/lib/betterdocs/generator/markdown/templates/section.md.erb +132 -0
- data/lib/betterdocs/global.rb +143 -0
- data/lib/betterdocs/json_params_representer.rb +37 -0
- data/lib/betterdocs/json_params_representer_collector.rb +48 -0
- data/lib/betterdocs/mix_into_controller.rb +19 -0
- data/lib/betterdocs/rake_tasks.rb +5 -0
- data/lib/betterdocs/representer.rb +42 -0
- data/lib/betterdocs/result_representer.rb +68 -0
- data/lib/betterdocs/result_representer_collector.rb +82 -0
- data/lib/betterdocs/section.rb +6 -0
- data/lib/betterdocs/tasks/doc.rake +55 -0
- data/lib/betterdocs/version.rb +8 -0
- data/spec/controller_dsl_spec.rb +143 -0
- data/spec/generator/markdown_spec.rb +5 -0
- data/spec/json_params_representer_spec.rb +79 -0
- data/spec/json_type_mapper_spec.rb +33 -0
- data/spec/result_representer_dsl_spec.rb +183 -0
- data/spec/result_representer_spec.rb +182 -0
- data/spec/spec_helper.rb +19 -0
- metadata +234 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
class Betterdocs::Dsl::Controller::Action::Param
|
2
|
+
extend Tins::DSLAccessor
|
3
|
+
include ::Betterdocs::Dsl::Common
|
4
|
+
|
5
|
+
def initialize(param_name, &block)
|
6
|
+
name param_name
|
7
|
+
block and instance_eval(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
dsl_accessor :name
|
11
|
+
|
12
|
+
dsl_accessor :value
|
13
|
+
|
14
|
+
dsl_accessor :required, true
|
15
|
+
|
16
|
+
dsl_accessor :description, 'TODO'
|
17
|
+
|
18
|
+
dsl_accessor :use_in_url, true
|
19
|
+
|
20
|
+
alias use_in_url? use_in_url
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Betterdocs::Dsl::Controller::Action::Response
|
2
|
+
include Betterdocs::Dsl::Common
|
3
|
+
extend Tins::DSLAccessor
|
4
|
+
|
5
|
+
def initialize(name = :default, &block)
|
6
|
+
@name = name.to_sym
|
7
|
+
@data_block = block || proc {}
|
8
|
+
end
|
9
|
+
|
10
|
+
dsl_accessor :name
|
11
|
+
|
12
|
+
def params
|
13
|
+
-> name { param(name).full?(:value) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def data
|
17
|
+
@data ||= instance_eval(&@data_block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def properties
|
21
|
+
representer.full? { |r| r.docs.nested_properties } || []
|
22
|
+
end
|
23
|
+
|
24
|
+
def links
|
25
|
+
representer.full? { |r| r.docs.nested_links } || []
|
26
|
+
end
|
27
|
+
|
28
|
+
def representer
|
29
|
+
if data
|
30
|
+
data.ask_and_send(:representer) ||
|
31
|
+
data.singleton_class.ancestors.find { |c|
|
32
|
+
Betterdocs::ResultRepresenter >= c && c.respond_to?(:docs)
|
33
|
+
# Actually it's more like
|
34
|
+
# Betterdocs::ResultRepresenter >= c && !c.singleton_class?
|
35
|
+
# in newer rubies.
|
36
|
+
# But singleton_class? is broken and private in ruby 2.1.x not
|
37
|
+
# existant in <= ruby 2.0.x and finally works in ruby 2.2.x.
|
38
|
+
# What a mess!
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json(*a)
|
44
|
+
my_data = data.ask_and_send(:to_hash) || data
|
45
|
+
my_data.to_json(*a)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'betterdocs/dsl/controller/controller_base'
|
2
|
+
|
3
|
+
class Betterdocs::Dsl::Controller::Controller < Betterdocs::Dsl::Controller::ControllerBase
|
4
|
+
def name
|
5
|
+
@name ||= controller.to_s.underscore.sub(/_controller\z/, '').to_sym
|
6
|
+
end
|
7
|
+
|
8
|
+
dsl_accessor :section
|
9
|
+
|
10
|
+
dsl_accessor :description, 'TODO'
|
11
|
+
|
12
|
+
def url
|
13
|
+
Betterdocs::Global.url_for(
|
14
|
+
controller: name,
|
15
|
+
action: :index,
|
16
|
+
format: 'json'
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def url_helpers
|
21
|
+
Betterdocs::Global.url_helpers
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
[ controller, '', "url: #{url}", '', description, '' ] * "\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_to_collector(collector)
|
29
|
+
collector.controller = self
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'betterdocs/dsl/common'
|
2
|
+
|
3
|
+
class Betterdocs::Dsl::Controller::ControllerBase
|
4
|
+
include Betterdocs::Dsl::Common
|
5
|
+
|
6
|
+
def self.inherited(klass)
|
7
|
+
klass.class_eval { extend Tins::DSLAccessor }
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(controller, &block)
|
11
|
+
controller(controller)
|
12
|
+
set_context controller
|
13
|
+
instance_eval(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
dsl_accessor :controller
|
17
|
+
|
18
|
+
def add_to_collector(collector)
|
19
|
+
raise NotImplementedError, 'add_to_collector needs to be implemented in subclass'
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Betterdocs::Dsl::JsonParams::Param < Betterdocs::Dsl::Representer
|
2
|
+
extend Tins::DSLAccessor
|
3
|
+
include Betterdocs::Dsl::Common
|
4
|
+
include Betterdocs::Dsl::Naming
|
5
|
+
|
6
|
+
dsl_accessor :description, 'TODO'
|
7
|
+
|
8
|
+
dsl_accessor :value, 'TODO'
|
9
|
+
|
10
|
+
dsl_accessor :types do [] end
|
11
|
+
|
12
|
+
dsl_accessor :required, true
|
13
|
+
|
14
|
+
def initialize(representer, name, options, &block)
|
15
|
+
super
|
16
|
+
types Betterdocs::Dsl::JsonTypeMapper.map_types(types)
|
17
|
+
end
|
18
|
+
|
19
|
+
def assign(result, object)
|
20
|
+
assign?(object) or return
|
21
|
+
result[name] = compute_value(object)
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute_value(object)
|
25
|
+
object.__send__(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_to_collector(collector)
|
29
|
+
collector.params[name] = self
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Betterdocs::Dsl::JsonTypeMapper
|
2
|
+
module_function
|
3
|
+
|
4
|
+
def derive_json_type_from(klass)
|
5
|
+
Class === klass or klass = klass.class
|
6
|
+
result = {
|
7
|
+
TrueClass => 'boolean',
|
8
|
+
FalseClass => 'boolean',
|
9
|
+
NilClass => 'null',
|
10
|
+
Numeric => 'number',
|
11
|
+
Array => 'array',
|
12
|
+
Hash => 'object',
|
13
|
+
String => 'string',
|
14
|
+
}.find { |match_class, json_type|
|
15
|
+
match_class >= klass and break json_type
|
16
|
+
} || 'undefined'
|
17
|
+
end
|
18
|
+
|
19
|
+
def map_types(types)
|
20
|
+
if Array === types and types.empty?
|
21
|
+
types = [ types ]
|
22
|
+
else
|
23
|
+
types = Array(types)
|
24
|
+
end
|
25
|
+
types.map { |t| derive_json_type_from(t) }.uniq.sort
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Betterdocs::Dsl::Naming
|
2
|
+
def initialize(*)
|
3
|
+
super
|
4
|
+
@options ||= {}
|
5
|
+
@below_path = []
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
def path
|
11
|
+
@below_path + [ public_name ]
|
12
|
+
end
|
13
|
+
|
14
|
+
def below_path(path)
|
15
|
+
dup.instance_eval do
|
16
|
+
@below_path = path
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def public_name
|
22
|
+
@options[:as] || name
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_name
|
26
|
+
path * '.'
|
27
|
+
end
|
28
|
+
|
29
|
+
def nesting_name
|
30
|
+
@below_path * '.'
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Betterdocs::Dsl::Representer
|
2
|
+
def initialize(representer, name, options, &block)
|
3
|
+
set_context @representer = representer
|
4
|
+
@name = name.to_sym
|
5
|
+
@options = options | {
|
6
|
+
if: -> { true },
|
7
|
+
unless: -> { false },
|
8
|
+
}
|
9
|
+
block and instance_eval(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
attr_reader :representer
|
15
|
+
|
16
|
+
def assign?(object)
|
17
|
+
object.instance_exec(&@options[:if]) &&
|
18
|
+
!object.instance_exec(&@options[:unless])
|
19
|
+
end
|
20
|
+
|
21
|
+
def assign(result, object)
|
22
|
+
raise NotImplementedError, 'assign needs to be implemented in subclass'
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_to_collector(collector)
|
26
|
+
raise NotImplementedError, 'add_to_collector needs to be implemented in subclass'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'betterdocs/dsl/representer'
|
2
|
+
require 'betterdocs/dsl/common'
|
3
|
+
require 'betterdocs/dsl/naming'
|
4
|
+
|
5
|
+
class Betterdocs::Dsl::Result::Link < Betterdocs::Dsl::Representer
|
6
|
+
extend Tins::DSLAccessor
|
7
|
+
include Betterdocs::Dsl::Common
|
8
|
+
include Betterdocs::Dsl::Naming
|
9
|
+
|
10
|
+
dsl_accessor :description, 'TODO'
|
11
|
+
|
12
|
+
dsl_accessor :templated, false
|
13
|
+
|
14
|
+
def url(&block)
|
15
|
+
if block
|
16
|
+
@url = block
|
17
|
+
elsif @url
|
18
|
+
@url
|
19
|
+
else
|
20
|
+
raise ArgumentError, 'link requires an URL'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def assign(result, object)
|
25
|
+
assign?(object) or return
|
26
|
+
link = {
|
27
|
+
'rel' => name.to_s,
|
28
|
+
'href' => object.instance_eval(&url).to_s,
|
29
|
+
}
|
30
|
+
templated and link['templated'] = true
|
31
|
+
result['links'].push(link)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_to_collector(collector)
|
35
|
+
collector.links[name] = self
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'betterdocs/dsl/representer'
|
2
|
+
require 'betterdocs/dsl/common'
|
3
|
+
require 'betterdocs/dsl/naming'
|
4
|
+
require 'betterdocs/dsl/json_type_mapper'
|
5
|
+
|
6
|
+
class Betterdocs::Dsl::Result::Property < Betterdocs::Dsl::Representer
|
7
|
+
extend Tins::DSLAccessor
|
8
|
+
include Betterdocs::Dsl::Common
|
9
|
+
include Betterdocs::Dsl::Naming
|
10
|
+
|
11
|
+
dsl_accessor :represent_with
|
12
|
+
|
13
|
+
dsl_accessor :description, 'TODO'
|
14
|
+
|
15
|
+
dsl_accessor :example, 'TODO'
|
16
|
+
|
17
|
+
dsl_accessor :types do [] end
|
18
|
+
|
19
|
+
def initialize(representer, name, options, &block)
|
20
|
+
super
|
21
|
+
types Betterdocs::Dsl::JsonTypeMapper.map_types(types)
|
22
|
+
if sr = sub_representer?
|
23
|
+
sr < Betterdocs::ResultRepresenter or
|
24
|
+
raise TypeError, "#{sr.inspect} is not a Betterdocs::Result subclass"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def sub_representer?
|
29
|
+
represent_with
|
30
|
+
end
|
31
|
+
|
32
|
+
def actual_property_name
|
33
|
+
(options[:as] || name).to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def assign(result, object)
|
37
|
+
assign?(object) or return
|
38
|
+
result[actual_property_name] = compute_value(object)
|
39
|
+
end
|
40
|
+
|
41
|
+
def compute_value(object)
|
42
|
+
value = object.__send__(name)
|
43
|
+
if !value.nil? && represent_with
|
44
|
+
represent_with.hashify(value)
|
45
|
+
else
|
46
|
+
value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_to_collector(collector)
|
51
|
+
collector.properties[name] = self
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Betterdocs
|
2
|
+
module Generator
|
3
|
+
module ConfigShortcuts
|
4
|
+
def config
|
5
|
+
Betterdocs::Global.config
|
6
|
+
end
|
7
|
+
|
8
|
+
def project_name
|
9
|
+
config.project_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def sections
|
13
|
+
Dir[config.api_controllers.to_s].each(&method(:load))
|
14
|
+
config.sections
|
15
|
+
end
|
16
|
+
|
17
|
+
def section(name)
|
18
|
+
name = name.to_sym
|
19
|
+
config.section(name) or STDERR.puts "Section #{name.inspect} does not exist: Link in readme file won't work."
|
20
|
+
"sections/#{name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def api_base_url
|
24
|
+
config
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Betterdocs
|
2
|
+
module Generator
|
3
|
+
class Markdown
|
4
|
+
include ::Betterdocs::Generator::ConfigShortcuts
|
5
|
+
require 'fileutils'
|
6
|
+
include FileUtils::Verbose
|
7
|
+
require 'term/ansicolor'
|
8
|
+
include Term::ANSIColor
|
9
|
+
|
10
|
+
def initialize(only: nil)
|
11
|
+
only and @only = Regexp.new(only)
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
if dir = config.output_directory.full?
|
16
|
+
generate_to dir
|
17
|
+
else
|
18
|
+
fail "Specify an output_directory in your configuration!"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_to(dirname)
|
23
|
+
configure_for_creation
|
24
|
+
prepare_dir dirname
|
25
|
+
create_sections(dirname)
|
26
|
+
create_readme dirname
|
27
|
+
create_assets
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def configure_for_creation
|
32
|
+
STDERR.puts "Setting asset_host to #{Betterdocs::Global.asset_host.inspect}."
|
33
|
+
Betterdocs.rails.configuration.action_controller.asset_host = Betterdocs::Global.asset_host
|
34
|
+
options = {
|
35
|
+
host: Betterdocs::Global.api_host,
|
36
|
+
protocol: Betterdocs::Global.api_protocol
|
37
|
+
}
|
38
|
+
STDERR.puts "Setting default_url_options to #{options.inspect}."
|
39
|
+
Betterdocs.rails.application.routes.default_url_options = options
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_sections(dirname)
|
44
|
+
cd dirname do
|
45
|
+
for section in sections.values
|
46
|
+
if @only
|
47
|
+
@only =~ section.name or next
|
48
|
+
end
|
49
|
+
STDERR.puts on_color(33, "Creating section #{section.name.inspect}.")
|
50
|
+
render_to "sections/#{section.name}.md", section_template, section.instance_eval('binding')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_readme(dirname)
|
57
|
+
name = 'README.md'
|
58
|
+
cd dirname do
|
59
|
+
STDERR.puts on_color(33, "Creating readme.")
|
60
|
+
render_to name, readme_template, binding
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_assets
|
66
|
+
config.each_asset do |src, dst|
|
67
|
+
STDERR.puts on_color(33, "Creating asset #{dst.inspect} from #{src.inspect}.")
|
68
|
+
mkdir_p File.dirname(dst)
|
69
|
+
cp src, dst
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def fail_while_rendering(template, exception)
|
76
|
+
message = blink(color(231, on_color(
|
77
|
+
124, " *** ERROR #{exception.class}: #{exception.message.inspect} in template ***")))
|
78
|
+
STDERR.puts message
|
79
|
+
Timeout.timeout(5, Timeout::Error) do
|
80
|
+
STDERR.print "Output long error message? (yes/NO) "
|
81
|
+
if STDIN.gets =~ /\Ay/i
|
82
|
+
STDERR.puts color(88, on_color(136, template)), message,
|
83
|
+
color(136, (%w[Backtrace:] + exception.backtrace) * "\n"),
|
84
|
+
message
|
85
|
+
end
|
86
|
+
end
|
87
|
+
rescue Timeout::Error
|
88
|
+
STDERR.puts "Nope…"
|
89
|
+
ensure
|
90
|
+
exit 1
|
91
|
+
end
|
92
|
+
|
93
|
+
def render_to(filename, template, binding)
|
94
|
+
File.open(filename, 'w') do |output|
|
95
|
+
rendered = ERB.new(template, nil, '-').result(binding)
|
96
|
+
output.write rendered
|
97
|
+
end
|
98
|
+
self
|
99
|
+
rescue => e
|
100
|
+
fail_while_rendering(template, e)
|
101
|
+
end
|
102
|
+
|
103
|
+
def default_templates_directory
|
104
|
+
File.join File.dirname(__FILE__), 'markdown', 'templates'
|
105
|
+
end
|
106
|
+
|
107
|
+
def read_template(filename)
|
108
|
+
STDERR.puts "Now reading #{filename.inspect}."
|
109
|
+
File.read(filename)
|
110
|
+
end
|
111
|
+
|
112
|
+
def provide_template(template_subpath)
|
113
|
+
if templates_directory = config.full?(:templates_directory)
|
114
|
+
path = File.expand_path(template_subpath, templates_directory)
|
115
|
+
File.file?(path) and return read_template(path)
|
116
|
+
end
|
117
|
+
path = File.expand_path(template_subpath, default_templates_directory)
|
118
|
+
File.file?(path) and return read_template(path)
|
119
|
+
message = "#{template_subpath.inspect} missing"
|
120
|
+
STDERR.puts " *** #{message}"
|
121
|
+
"[#{message}]"
|
122
|
+
end
|
123
|
+
|
124
|
+
def readme_template
|
125
|
+
provide_template 'README.md.erb'
|
126
|
+
end
|
127
|
+
memoize_method :readme_template
|
128
|
+
|
129
|
+
def section_template
|
130
|
+
provide_template 'section.md.erb'
|
131
|
+
end
|
132
|
+
memoize_method :section_template
|
133
|
+
|
134
|
+
def prepare_dir(dirname)
|
135
|
+
dirname.present? or raise ArgumentError,
|
136
|
+
"#{dirname.inspect} should be an explicite output dirname"
|
137
|
+
begin
|
138
|
+
stat = File.stat(dirname)
|
139
|
+
if stat.directory?
|
140
|
+
rm_rf Dir[dirname.to_s + '/**/*']
|
141
|
+
else
|
142
|
+
raise ArgumentError, "#{dirname.inspect} is not a directory"
|
143
|
+
end
|
144
|
+
rescue Errno::ENOENT
|
145
|
+
end
|
146
|
+
mkdir_p "#{dirname}/sections"
|
147
|
+
self
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|