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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +5 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +9 -0
  6. data/COPYING +202 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE +202 -0
  9. data/README.md +124 -0
  10. data/Rakefile +25 -0
  11. data/VERSION +1 -0
  12. data/betterdocs.gemspec +48 -0
  13. data/lib/betterdocs.rb +27 -0
  14. data/lib/betterdocs/controller_collector.rb +50 -0
  15. data/lib/betterdocs/dsl.rb +9 -0
  16. data/lib/betterdocs/dsl/common.rb +26 -0
  17. data/lib/betterdocs/dsl/controller.rb +9 -0
  18. data/lib/betterdocs/dsl/controller/action.rb +126 -0
  19. data/lib/betterdocs/dsl/controller/action/param.rb +25 -0
  20. data/lib/betterdocs/dsl/controller/action/response.rb +47 -0
  21. data/lib/betterdocs/dsl/controller/controller.rb +31 -0
  22. data/lib/betterdocs/dsl/controller/controller_base.rb +21 -0
  23. data/lib/betterdocs/dsl/json_params.rb +8 -0
  24. data/lib/betterdocs/dsl/json_params/param.rb +31 -0
  25. data/lib/betterdocs/dsl/json_type_mapper.rb +27 -0
  26. data/lib/betterdocs/dsl/naming.rb +32 -0
  27. data/lib/betterdocs/dsl/representer.rb +29 -0
  28. data/lib/betterdocs/dsl/result.rb +10 -0
  29. data/lib/betterdocs/dsl/result/collection_property.rb +9 -0
  30. data/lib/betterdocs/dsl/result/link.rb +37 -0
  31. data/lib/betterdocs/dsl/result/property.rb +53 -0
  32. data/lib/betterdocs/generator/config_shortcuts.rb +28 -0
  33. data/lib/betterdocs/generator/markdown.rb +151 -0
  34. data/lib/betterdocs/generator/markdown/templates/README.md.erb +9 -0
  35. data/lib/betterdocs/generator/markdown/templates/section.md.erb +132 -0
  36. data/lib/betterdocs/global.rb +143 -0
  37. data/lib/betterdocs/json_params_representer.rb +37 -0
  38. data/lib/betterdocs/json_params_representer_collector.rb +48 -0
  39. data/lib/betterdocs/mix_into_controller.rb +19 -0
  40. data/lib/betterdocs/rake_tasks.rb +5 -0
  41. data/lib/betterdocs/representer.rb +42 -0
  42. data/lib/betterdocs/result_representer.rb +68 -0
  43. data/lib/betterdocs/result_representer_collector.rb +82 -0
  44. data/lib/betterdocs/section.rb +6 -0
  45. data/lib/betterdocs/tasks/doc.rake +55 -0
  46. data/lib/betterdocs/version.rb +8 -0
  47. data/spec/controller_dsl_spec.rb +143 -0
  48. data/spec/generator/markdown_spec.rb +5 -0
  49. data/spec/json_params_representer_spec.rb +79 -0
  50. data/spec/json_type_mapper_spec.rb +33 -0
  51. data/spec/result_representer_dsl_spec.rb +183 -0
  52. data/spec/result_representer_spec.rb +182 -0
  53. data/spec/spec_helper.rb +19 -0
  54. 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,8 @@
1
+ module Betterdocs
2
+ module Dsl
3
+ module JsonParams
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'betterdocs/dsl/json_params/param'
@@ -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,10 @@
1
+ module Betterdocs
2
+ module Dsl
3
+ module Result
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'betterdocs/dsl/result/property'
9
+ require 'betterdocs/dsl/result/collection_property'
10
+ require 'betterdocs/dsl/result/link'
@@ -0,0 +1,9 @@
1
+ require 'betterdocs/dsl/result/property'
2
+
3
+ class Betterdocs::Dsl::Result::CollectionProperty < Betterdocs::Dsl::Result::Property
4
+ def compute_value(object)
5
+ object.__send__(name).to_a.compact.map do |v|
6
+ represent_with.hashify(v)
7
+ end
8
+ end
9
+ end
@@ -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